mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc6f8a64b5 |
@@ -1,84 +0,0 @@
|
|||||||
{
|
|
||||||
"projectName": "server",
|
|
||||||
"projectOwner": "sasjs",
|
|
||||||
"repoType": "github",
|
|
||||||
"repoHost": "https://github.com",
|
|
||||||
"files": [
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"imageSize": 100,
|
|
||||||
"commit": true,
|
|
||||||
"commitConvention": "angular",
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"login": "saadjutt01",
|
|
||||||
"name": "Saad Jutt",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
|
|
||||||
"profile": "https://github.com/saadjutt01",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "sabhas",
|
|
||||||
"name": "Sabir Hassan",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
|
|
||||||
"profile": "https://github.com/sabhas",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "YuryShkoda",
|
|
||||||
"name": "Yury Shkoda",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
|
|
||||||
"profile": "https://www.erudicat.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "medjedovicm",
|
|
||||||
"name": "Mihajlo Medjedovic",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
|
|
||||||
"profile": "https://github.com/medjedovicm",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "allanbowe",
|
|
||||||
"name": "Allan Bowe",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
|
|
||||||
"profile": "https://4gl.io/",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"doc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "VladislavParhomchik",
|
|
||||||
"name": "Vladislav Parhomchik",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
|
|
||||||
"profile": "https://github.com/VladislavParhomchik",
|
|
||||||
"contributions": [
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "kknapen",
|
|
||||||
"name": "Koen Knapen",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/78609432?v=4",
|
|
||||||
"profile": "https://github.com/kknapen",
|
|
||||||
"contributions": [
|
|
||||||
"userTesting"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"contributorsPerLine": 7,
|
|
||||||
"skipCi": true
|
|
||||||
}
|
|
||||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [sasjs]
|
|
||||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
run: npm run lint-web
|
run: npm run lint-web
|
||||||
|
|
||||||
build-api:
|
build-api:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -54,10 +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}}
|
|
||||||
RUN_TIMES: 'sas,js'
|
|
||||||
SAS_PATH: '/some/path/to/sas'
|
|
||||||
NODE_PATH: '/some/path/to/node'
|
|
||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
working-directory: ./api
|
working-directory: ./api
|
||||||
@@ -66,7 +62,7 @@ jobs:
|
|||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
build-web:
|
build-web:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
29
.github/workflows/release.yml
vendored
29
.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-22.04
|
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.GITHUB_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'"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": ["autoexec", "initialising"]
|
"cSpell.words": [
|
||||||
|
"autoexec"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1135
CHANGELOG.md
1135
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
|||||||
## Issue
|
|
||||||
|
|
||||||
Link any related issue(s) in this section.
|
|
||||||
|
|
||||||
## Intent
|
|
||||||
|
|
||||||
What this PR intends to achieve.
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
What code changes have been made to achieve the intent.
|
|
||||||
|
|
||||||
## Checks
|
|
||||||
|
|
||||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
|
||||||
- [ ] Any new functionality has been unit tested.
|
|
||||||
- [ ] All unit tests are passing (`npm test`).
|
|
||||||
- [ ] All CI checks are green.
|
|
||||||
- [ ] Reviewer is assigned.
|
|
||||||
200
README.md
200
README.md
@@ -1,11 +1,5 @@
|
|||||||
# SASjs Server
|
# SASjs Server
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
||||||
|
|
||||||
[](#contributors-)
|
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
||||||
|
|
||||||
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
|
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
|
||||||
|
|
||||||
- Virtual filesystem for storing SAS programs and other content
|
- Virtual filesystem for storing SAS programs and other content
|
||||||
@@ -54,102 +48,9 @@ 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=
|
||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
|
||||||
# Priority is given to the runtime that comes first in the string.
|
|
||||||
# Possible options at the moment are sas, js, py and r
|
|
||||||
|
|
||||||
# This string sets the priority of the available analytic runtimes
|
|
||||||
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
|
|
||||||
# For each option provided, there should be a corresponding path,
|
|
||||||
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
|
|
||||||
# Priority is given to runtimes earlier in the string
|
|
||||||
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
|
|
||||||
RUN_TIMES=
|
|
||||||
|
|
||||||
# Path to SAS executable (sas.exe / sas.sh)
|
|
||||||
SAS_PATH=/path/to/sas/executable.exe
|
|
||||||
|
|
||||||
# Path to Node.js executable
|
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
|
||||||
|
|
||||||
# Path to Python executable
|
|
||||||
PYTHON_PATH=/usr/bin/python
|
|
||||||
|
|
||||||
# Path to R executable
|
|
||||||
R_PATH=/usr/bin/Rscript
|
|
||||||
|
|
||||||
# Path to working directory
|
|
||||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
|
||||||
SASJS_ROOT=./sasjs_root
|
|
||||||
|
|
||||||
|
|
||||||
# This location is for files, sasjs packages and appStreamConfig.json
|
|
||||||
DRIVE_LOCATION=./sasjs_root/drive
|
|
||||||
|
|
||||||
|
|
||||||
# options: [http|https] default: http
|
|
||||||
PROTOCOL=
|
|
||||||
|
|
||||||
# default: 5000
|
|
||||||
PORT=
|
|
||||||
|
|
||||||
# options: [sas9|sasviya]
|
|
||||||
# If not present, mocking function is disabled
|
|
||||||
MOCK_SERVERTYPE=
|
|
||||||
|
|
||||||
# default: /api/mocks
|
|
||||||
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
|
|
||||||
# Server will automatically use subdirectory accordingly
|
|
||||||
STATIC_MOCK_LOCATION=
|
|
||||||
|
|
||||||
#
|
|
||||||
## Additional SAS Options
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
|
|
||||||
# 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
|
|
||||||
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
|
|
||||||
SAS_OPTIONS= -NOXCMD
|
|
||||||
SASV9_OPTIONS= -NOXCMD
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
## Additional Web Server Options
|
|
||||||
#
|
|
||||||
|
|
||||||
# ENV variables for PROTOCOL: `https`
|
|
||||||
PRIVATE_KEY=privkey.pem (required)
|
|
||||||
CERT_CHAIN=certificate.pem (required)
|
|
||||||
CA_ROOT=fullchain.pem (optional)
|
|
||||||
|
|
||||||
## ENV variables required for MODE: `server`
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
|
||||||
|
|
||||||
# options: [mongodb|cosmos_mongodb] default: mongodb
|
|
||||||
DB_TYPE=
|
|
||||||
|
|
||||||
# AUTH_PROVIDERS options: [ldap] default: ``
|
|
||||||
AUTH_PROVIDERS=
|
|
||||||
|
|
||||||
## ENV variables required for AUTH_MECHANISM: `ldap`
|
|
||||||
LDAP_URL= <LDAP_SERVER_URL>
|
|
||||||
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
|
|
||||||
LDAP_BIND_PASSWORD = <password>
|
|
||||||
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
|
||||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
|
||||||
|
|
||||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||||
CORS=
|
CORS=
|
||||||
@@ -157,56 +58,41 @@ CORS=
|
|||||||
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||||
WHITELIST=
|
WHITELIST=
|
||||||
|
|
||||||
# HELMET Cross Origin Embedder Policy
|
# options: [http|https] default: http
|
||||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
PROTOCOL=
|
||||||
# options: [true|false] default: true
|
|
||||||
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
|
|
||||||
HELMET_COEP=
|
|
||||||
|
|
||||||
# HELMET Content Security Policy
|
# default: 5000
|
||||||
# Path to a json file containing HELMET `contentSecurityPolicy` directives
|
PORT=
|
||||||
# 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
|
|
||||||
|
|
||||||
# To prevent brute force attack on login route we have implemented rate limiter
|
|
||||||
# Only valid for MODE: server
|
|
||||||
# Following are configurable env variable rate limiter
|
|
||||||
|
|
||||||
# After this, access is blocked for 1 day
|
|
||||||
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
|
|
||||||
|
|
||||||
|
|
||||||
# After this, access is blocked for an hour
|
# optional
|
||||||
# Store number for 24 days since first fail
|
# for MODE: `desktop`, prompts user
|
||||||
# Once a successful login is attempted, it resets
|
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
|
||||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
|
||||||
# Name of the admin user that will be created on startup if not exists already
|
|
||||||
# Default is `secretuser`
|
|
||||||
ADMIN_USERNAME=secretuser
|
|
||||||
|
|
||||||
# Temporary password for the ADMIN_USERNAME, which is in place until the first login
|
# optional
|
||||||
# Default is `secretpassword`
|
# for MODE: `desktop`, prompts user
|
||||||
ADMIN_PASSWORD_INITIAL=secretpassword
|
# for MODE: `server` defaults to /tmp
|
||||||
|
DRIVE_PATH=/tmp
|
||||||
|
|
||||||
# Specify whether app has to reset the ADMIN_USERNAME's password or not
|
# ENV variables required for PROTOCOL: `https`
|
||||||
# Default is NO. Possible options are YES and NO
|
PRIVATE_KEY=privkey.pem
|
||||||
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
|
FULL_CHAIN=fullchain.pem
|
||||||
ADMIN_PASSWORD_RESET=NO
|
|
||||||
|
|
||||||
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
# ENV variables required for MODE: `server`
|
||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
LOG_FORMAT_MORGAN=
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
|
AUTH_CODE_SECRET=<secret>
|
||||||
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
# This location is for server logs with classical UNIX logrotate behavior
|
# SAS Options
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
|
||||||
|
# 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
|
||||||
|
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
|
||||||
|
SAS_OPTIONS= -NOXCMD
|
||||||
|
SASV9_OPTIONS= -NOXCMD
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -221,7 +107,7 @@ Normally the server process will stop when your terminal dies. To keep it going
|
|||||||
|
|
||||||
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
|
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
|
||||||
|
|
||||||
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
|
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
|
||||||
|
|
||||||
### PM2
|
### PM2
|
||||||
|
|
||||||
@@ -230,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
|
||||||
```
|
```
|
||||||
@@ -264,29 +150,3 @@ The following credentials can be used for the initial connection to SASjs/server
|
|||||||
- CLIENTID: `clientID1`
|
- CLIENTID: `clientID1`
|
||||||
- USERNAME: `secretuser`
|
- USERNAME: `secretuser`
|
||||||
- PASSWORD: `secretpassword`
|
- PASSWORD: `secretpassword`
|
||||||
|
|
||||||
## Contributors ✨
|
|
||||||
|
|
||||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
<!-- markdownlint-disable -->
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Saad Jutt</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Tests">⚠️</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=sabhas" title="Tests">⚠️</a></td>
|
|
||||||
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Tests">⚠️</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Tests">⚠️</a></td>
|
|
||||||
<td align="center"><a href="https://4gl.io/"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vladislav Parhomchik</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=VladislavParhomchik" title="Tests">⚠️</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/kknapen"><img src="https://avatars.githubusercontent.com/u/78609432?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Koen Knapen</b></sub></a><br /><a href="#userTesting-kknapen" title="User Testing">📓</a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- markdownlint-restore -->
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
||||||
|
|
||||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
|
||||||
|
|||||||
@@ -1,47 +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
|
||||||
ALLOWED_DOMAIN=<just domain e.g. example.com >
|
|
||||||
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
|
||||||
CERT_CHAIN=certificate.pem
|
FULL_CHAIN=fullchain.pem
|
||||||
CA_ROOT=fullchain.pem
|
|
||||||
|
|
||||||
PORT=[5000] default value is 5000
|
PORT=[5000] default value is 5000
|
||||||
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
AUTH_CODE_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
|
||||||
DB_TYPE=[mongodb|cosmos_mongodb] default considered as mongodb
|
|
||||||
|
|
||||||
AUTH_PROVIDERS=[ldap]
|
|
||||||
|
|
||||||
LDAP_URL= <LDAP_SERVER_URL>
|
|
||||||
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
|
|
||||||
LDAP_BIND_PASSWORD = <password>
|
|
||||||
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
|
||||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
|
||||||
|
|
||||||
#default value is 100
|
|
||||||
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
|
|
||||||
|
|
||||||
#default value is 10
|
|
||||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
|
|
||||||
|
|
||||||
ADMIN_USERNAME=secretuser
|
|
||||||
ADMIN_PASSWORD_INITIAL=secretpassword
|
|
||||||
ADMIN_PASSWORD_RESET=NO
|
|
||||||
|
|
||||||
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
DRIVE_PATH=./tmp
|
||||||
PYTHON_PATH=/usr/bin/python
|
|
||||||
R_PATH=/usr/bin/Rscript
|
|
||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
|
||||||
DRIVE_LOCATION=./sasjs_root/drive
|
|
||||||
|
|
||||||
LOG_FORMAT_MORGAN=common
|
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v16.15.1
|
v16.14.0
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"img-src": ["'self'", "data:"],
|
|
||||||
"script-src": ["'self'", "'unsafe-inline'"],
|
|
||||||
"script-src-attr": ["'self'", "'unsafe-inline'"]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
You have signed in.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
You have signed out.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="initial-scale=1" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
|
||||||
<!--form container-->
|
|
||||||
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
|
|
||||||
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
|
||||||
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
|
||||||
|
|
||||||
<span class="userid">
|
|
||||||
|
|
||||||
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
|
|
||||||
</span>
|
|
||||||
<span class="password">
|
|
||||||
|
|
||||||
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
|
|
||||||
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Public access has been denied.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"title": "Log Off SAS Demo User"
|
|
||||||
19487
api/package-lock.json
generated
19487
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,10 @@
|
|||||||
"description": "Api of SASjs server",
|
"description": "Api of SASjs server",
|
||||||
"main": "./src/server.ts",
|
"main": "./src/server.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore && npm run downloadMacros",
|
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||||
"prestart": "npm run initial",
|
"prestart": "npm run initial",
|
||||||
"prebuild": "npm run initial",
|
"prebuild": "npm run initial",
|
||||||
"start": "NODE_ENV=development nodemon ./src/server.ts",
|
"start": "nodemon ./src/server.ts",
|
||||||
"start:prod": "node ./build/src/server.js",
|
"start:prod": "node ./build/src/server.js",
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
"postbuild": "npm run copy:files",
|
"postbuild": "npm run copy:files",
|
||||||
@@ -17,21 +17,20 @@
|
|||||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"exe": "npm run build && pkg .",
|
"exe": "npm run build && pkg .",
|
||||||
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sas:copy && npm run web:copy",
|
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||||
"public:copy": "cp -r ./public/ ./build/public/",
|
"public:copy": "cp -r ./public/ ./build/public/",
|
||||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
||||||
"sas:copy": "cp -r ./sas/ ./build/sas/",
|
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
|
||||||
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
||||||
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
||||||
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts",
|
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
|
||||||
"downloadMacros": "ts-node ./scripts/downloadMacros.ts"
|
|
||||||
},
|
},
|
||||||
"bin": "./build/src/server.js",
|
"bin": "./build/src/server.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"assets": [
|
"assets": [
|
||||||
"./build/public/**/*",
|
"./build/public/**/*",
|
||||||
"./build/sasjsbuild/**/*",
|
"./build/sasjsbuild/**/*",
|
||||||
"./build/sas/**/*",
|
"./build/sasjscore/**/*",
|
||||||
"./web/build/**/*"
|
"./web/build/**/*"
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
@@ -48,64 +47,53 @@
|
|||||||
},
|
},
|
||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.59.7",
|
"@sasjs/core": "^4.19.0",
|
||||||
"@sasjs/utils": "^3.5.2",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^5.1.0",
|
"cookie-parser": "^1.4.6",
|
||||||
"cookie-parser": "^1.4.7",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.2",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.18.2",
|
|
||||||
"helmet": "^5.0.2",
|
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"ldapjs": "2.3.3",
|
"mongoose": "^6.0.12",
|
||||||
"mongoose": "^6.13.8",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.1",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.3",
|
||||||
"rate-limiter-flexible": "2.4.1",
|
"swagger-ui-express": "^4.1.6"
|
||||||
"rotating-file-stream": "^3.0.4",
|
|
||||||
"swagger-ui-express": "4.3.0",
|
|
||||||
"unzipper": "^0.12.3",
|
|
||||||
"url": "^0.10.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.5.0",
|
|
||||||
"@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/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/jest": "^29.5.0",
|
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/ldapjs": "^2.2.4",
|
"@types/mongoose-sequence": "^3.0.6",
|
||||||
"@types/morgan": "^1.9.3",
|
"@types/morgan": "^1.9.3",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^15.12.2",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
"@types/unzipper": "^0.10.5",
|
"dotenv": "^10.0.0",
|
||||||
"adm-zip": "^0.5.9",
|
|
||||||
"axios": "^1.12.2",
|
|
||||||
"csrf": "^3.1.0",
|
|
||||||
"dotenv": "^16.0.1",
|
|
||||||
"http-headers-validation": "^0.0.1",
|
"http-headers-validation": "^0.0.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "8.11.4",
|
"mongodb-memory-server": "^8.0.0",
|
||||||
"nodejs-file-downloader": "4.10.2",
|
"nodemon": "^2.0.7",
|
||||||
"nodemon": "^3.0.0",
|
"pkg": "5.5.2",
|
||||||
"pkg": "5.6.0",
|
"prettier": "^2.3.1",
|
||||||
"prettier": "^3.0.0",
|
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^27.0.3",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"tsoa": "3.14.1",
|
"tsoa": "3.14.1",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^4.3.2"
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"sasjs_root/**/*"
|
"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
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,12 @@ import {
|
|||||||
readFile,
|
readFile,
|
||||||
SASJsFileType
|
SASJsFileType
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
|
import { apiRoot, sysInitCompiledPath } from '../src/utils'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
const compiledSystemInit = async (systemInit: string) =>
|
const compiledSystemInit = async (systemInit: string) =>
|
||||||
'options ls=max ps=max;\n' +
|
'options ps=max;\n' +
|
||||||
(await loadDependenciesFile({
|
(await loadDependenciesFile({
|
||||||
fileContent: systemInit,
|
fileContent: systemInit,
|
||||||
type: SASJsFileType.job,
|
type: SASJsFileType.job,
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import {
|
|||||||
listFilesInFolder
|
listFilesInFolder
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import {
|
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
|
||||||
apiRoot,
|
|
||||||
sasJSCoreMacros,
|
|
||||||
sasJSCoreMacrosInfo
|
|
||||||
} from '../src/utils/file'
|
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import Downloader from 'nodejs-file-downloader'
|
|
||||||
import { createFile, listFilesInFolder } from '@sasjs/utils'
|
|
||||||
|
|
||||||
import { sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils/file'
|
|
||||||
|
|
||||||
export const downloadMacros = async () => {
|
|
||||||
const url =
|
|
||||||
'https://api.github.com/repos/yabwon/SAS_PACKAGES/contents/SPF/Macros'
|
|
||||||
|
|
||||||
console.info(`Downloading macros from ${url}`)
|
|
||||||
|
|
||||||
await axios
|
|
||||||
.get(url)
|
|
||||||
.then(async (res) => {
|
|
||||||
await downloadFiles(res.data)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
throw new Error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadFiles = async function (fileList: any) {
|
|
||||||
for (const file of fileList) {
|
|
||||||
const downloader = new Downloader({
|
|
||||||
url: file.download_url,
|
|
||||||
directory: sasJSCoreMacros,
|
|
||||||
fileName: file.path.replace(/^SPF\/Macros/, ''),
|
|
||||||
cloneFiles: false
|
|
||||||
})
|
|
||||||
await downloader.download()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
|
||||||
|
|
||||||
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadMacros()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Express } from 'express'
|
|
||||||
import cors from 'cors'
|
|
||||||
import { CorsType } from '../utils'
|
|
||||||
|
|
||||||
export const configureCors = (app: Express) => {
|
|
||||||
const { CORS, WHITELIST } = process.env
|
|
||||||
|
|
||||||
if (CORS === CorsType.ENABLED) {
|
|
||||||
const whiteList: string[] = []
|
|
||||||
WHITELIST?.split(' ')
|
|
||||||
?.filter((url) => !!url)
|
|
||||||
.forEach((url) => {
|
|
||||||
if (url.startsWith('http'))
|
|
||||||
// removing trailing slash of URLs listing for CORS
|
|
||||||
whiteList.push(url.replace(/\/$/, ''))
|
|
||||||
})
|
|
||||||
|
|
||||||
process.logger.info('All CORS Requests are enabled for:', whiteList)
|
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Express, CookieOptions } from 'express'
|
|
||||||
import mongoose from 'mongoose'
|
|
||||||
import session from 'express-session'
|
|
||||||
import MongoStore from 'connect-mongo'
|
|
||||||
|
|
||||||
import { DatabaseType, ModeType, ProtocolType } from '../utils'
|
|
||||||
|
|
||||||
export const configureExpressSession = (app: Express) => {
|
|
||||||
const { MODE, DB_TYPE } = process.env
|
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
|
||||||
let store: MongoStore | undefined
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
if (DB_TYPE === DatabaseType.COSMOS_MONGODB) {
|
|
||||||
// COSMOS DB requires specific connection options (compatibility mode)
|
|
||||||
// See: https://www.npmjs.com/package/connect-mongo#set-the-compatibility-mode
|
|
||||||
store = MongoStore.create({
|
|
||||||
client: mongoose.connection!.getClient() as any,
|
|
||||||
autoRemove: 'interval'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
store = MongoStore.create({
|
|
||||||
client: mongoose.connection!.getClient() as any
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
|
|
||||||
const cookieOptions: CookieOptions = {
|
|
||||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
|
||||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
||||||
domain: ALLOWED_DOMAIN?.trim() || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
secret: process.secrets.SESSION_SECRET,
|
|
||||||
saveUninitialized: false, // don't create session until something stored
|
|
||||||
resave: false, //don't save session if unmodified
|
|
||||||
store,
|
|
||||||
cookie: cookieOptions
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import { Express } from 'express'
|
|
||||||
import morgan from 'morgan'
|
|
||||||
import { createStream } from 'rotating-file-stream'
|
|
||||||
import { generateTimestamp } from '@sasjs/utils'
|
|
||||||
import { getLogFolder } from '../utils'
|
|
||||||
|
|
||||||
export const configureLogger = (app: Express) => {
|
|
||||||
const { LOG_FORMAT_MORGAN } = process.env
|
|
||||||
|
|
||||||
let options
|
|
||||||
if (
|
|
||||||
process.env.NODE_ENV !== 'development' &&
|
|
||||||
process.env.NODE_ENV !== 'test'
|
|
||||||
) {
|
|
||||||
const timestamp = generateTimestamp()
|
|
||||||
const filename = `${timestamp}.log`
|
|
||||||
const logsFolder = getLogFolder()
|
|
||||||
|
|
||||||
// create a rotating write stream
|
|
||||||
var accessLogStream = createStream(filename, {
|
|
||||||
interval: '1d', // rotate daily
|
|
||||||
path: logsFolder
|
|
||||||
})
|
|
||||||
|
|
||||||
process.logger.info('Writing Logs to :', path.join(logsFolder, filename))
|
|
||||||
|
|
||||||
options = { stream: accessLogStream }
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup the logger
|
|
||||||
app.use(morgan(LOG_FORMAT_MORGAN as string, options))
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Express } from 'express'
|
|
||||||
import { getEnvCSPDirectives } from '../utils/parseHelmetConfig'
|
|
||||||
import { HelmetCoepType, ProtocolType } from '../utils'
|
|
||||||
import helmet from 'helmet'
|
|
||||||
|
|
||||||
export const configureSecurity = (app: Express) => {
|
|
||||||
const { PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } = process.env
|
|
||||||
|
|
||||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
|
||||||
HELMET_CSP_CONFIG_PATH
|
|
||||||
)
|
|
||||||
if (PROTOCOL === ProtocolType.HTTP)
|
|
||||||
cspConfigJson['upgrade-insecure-requests'] = null
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
helmet({
|
|
||||||
contentSecurityPolicy: {
|
|
||||||
directives: {
|
|
||||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
|
||||||
...cspConfigJson
|
|
||||||
}
|
|
||||||
},
|
|
||||||
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './configureCors'
|
|
||||||
export * from './configureExpressSession'
|
|
||||||
export * from './configureLogger'
|
|
||||||
export * from './configureSecurity'
|
|
||||||
@@ -1,88 +1,52 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
|
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 {
|
import {
|
||||||
|
connectDB,
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
createWeboutSasFile,
|
getWebBuildFolderPath,
|
||||||
getFilesFolder,
|
|
||||||
getPackagesFolder,
|
|
||||||
getWebBuildFolder,
|
|
||||||
instantiateLogger,
|
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
ReturnCode,
|
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFilesFolder,
|
setupFolders
|
||||||
setupPackagesFolder,
|
|
||||||
setupUserAutoExec,
|
|
||||||
verifyEnvVariables
|
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
|
||||||
configureCors,
|
|
||||||
configureExpressSession,
|
|
||||||
configureLogger,
|
|
||||||
configureSecurity
|
|
||||||
} from './app-modules'
|
|
||||||
import { folderExists } from '@sasjs/utils'
|
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
instantiateLogger()
|
|
||||||
|
|
||||||
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
const { MODE, CORS, WHITELIST } = process.env
|
||||||
|
|
||||||
|
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||||
|
const whiteList: string[] = []
|
||||||
|
WHITELIST?.split(' ')
|
||||||
|
?.filter((url) => !!url)
|
||||||
|
.forEach((url) => {
|
||||||
|
if (url.startsWith('http'))
|
||||||
|
// removing trailing slash of URLs listing for CORS
|
||||||
|
whiteList.push(url.replace(/\/$/, ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('All CORS Requests are enabled for:', whiteList)
|
||||||
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(cookieParser())
|
||||||
|
app.use(morgan('tiny'))
|
||||||
|
app.use(express.json({ limit: '100mb' }))
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
process.logger.error(err.stack)
|
console.error(err.stack)
|
||||||
res.status(500).send('Something broke!')
|
res.status(500).send('Something broke!')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default setProcessVariables().then(async () => {
|
export default setProcessVariables().then(async () => {
|
||||||
app.use(cookieParser())
|
await setupFolders()
|
||||||
|
await copySASjsCore()
|
||||||
configureLogger(app)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Handle security and origin *
|
|
||||||
***********************************/
|
|
||||||
configureSecurity(app)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Enabling CORS *
|
|
||||||
***********************************/
|
|
||||||
configureCors(app)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* DB Connection & *
|
|
||||||
* Express Sessions *
|
|
||||||
* With Mongo Store *
|
|
||||||
***********************************/
|
|
||||||
configureExpressSession(app)
|
|
||||||
|
|
||||||
app.use(express.json({ limit: '100mb' }))
|
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
|
||||||
|
|
||||||
// Body parser is used for decoding the formdata on POST request.
|
|
||||||
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
|
||||||
app.use(express.urlencoded({ extended: true }))
|
|
||||||
|
|
||||||
await setupUserAutoExec()
|
|
||||||
|
|
||||||
if (!(await folderExists(getFilesFolder()))) await setupFilesFolder()
|
|
||||||
|
|
||||||
if (!(await folderExists(getPackagesFolder()))) await setupPackagesFolder()
|
|
||||||
|
|
||||||
const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos')
|
|
||||||
if (await folderExists(sasautosPath)) {
|
|
||||||
process.logger.warn(
|
|
||||||
`SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
await copySASjsCore()
|
|
||||||
await createWeboutSasFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading these modules after setting up variables due to
|
// loading these modules after setting up variables due to
|
||||||
// multer's usage of process var process.driveLoc
|
// multer's usage of process var process.driveLoc
|
||||||
@@ -93,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,27 +1,14 @@
|
|||||||
import express from 'express'
|
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
||||||
import {
|
|
||||||
Security,
|
|
||||||
Route,
|
|
||||||
Tags,
|
|
||||||
Example,
|
|
||||||
Post,
|
|
||||||
Patch,
|
|
||||||
Request,
|
|
||||||
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,
|
||||||
getTokensFromDB,
|
|
||||||
removeTokensInDB,
|
removeTokensInDB,
|
||||||
saveTokensInDB
|
saveTokensInDB
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import Client from '../model/Client'
|
|
||||||
import User from '../model/User'
|
|
||||||
|
|
||||||
@Route('SASjsApi/auth')
|
@Route('SASjsApi/auth')
|
||||||
@Tags('Auth')
|
@Tags('Auth')
|
||||||
@@ -37,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
|
||||||
*
|
*
|
||||||
@@ -75,18 +76,30 @@ export class AuthController {
|
|||||||
public async logout(@Query() @Hidden() data?: InfoJWT) {
|
public async logout(@Query() @Hidden() data?: InfoJWT) {
|
||||||
return logout(data!)
|
return logout(data!)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
||||||
* @summary Update user's password.
|
const { username, password, clientId } = data
|
||||||
*/
|
|
||||||
@Security('bearerAuth')
|
// Authenticate User
|
||||||
@Patch('updatePassword')
|
const user = await User.findOne({ username })
|
||||||
public async updatePassword(
|
if (!user) throw new Error('Username is not found.')
|
||||||
@Request() req: express.Request,
|
|
||||||
@Body() body: UpdatePasswordPayload
|
const validPass = user.comparePassword(password)
|
||||||
) {
|
if (!validPass) throw new Error('Invalid password.')
|
||||||
return updatePassword(req, body)
|
|
||||||
|
// 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> => {
|
||||||
@@ -100,26 +113,8 @@ const token = async (data: any): Promise<TokenResponse> => {
|
|||||||
|
|
||||||
AuthController.deleteCode(userInfo.userId, clientId)
|
AuthController.deleteCode(userInfo.userId, clientId)
|
||||||
|
|
||||||
// get tokens from DB
|
const accessToken = generateAccessToken(userInfo)
|
||||||
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
|
const refreshToken = generateRefreshToken(userInfo)
|
||||||
if (existingTokens) {
|
|
||||||
return {
|
|
||||||
accessToken: existingTokens.accessToken,
|
|
||||||
refreshToken: existingTokens.refreshToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await Client.findOne({ clientId })
|
|
||||||
if (!client) throw new Error('Invalid clientId.')
|
|
||||||
|
|
||||||
const accessToken = generateAccessToken(
|
|
||||||
userInfo,
|
|
||||||
client.accessTokenExpiration
|
|
||||||
)
|
|
||||||
const refreshToken = generateRefreshToken(
|
|
||||||
userInfo,
|
|
||||||
client.refreshTokenExpiration
|
|
||||||
)
|
|
||||||
|
|
||||||
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
||||||
|
|
||||||
@@ -127,17 +122,8 @@ const token = async (data: any): Promise<TokenResponse> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
||||||
const client = await Client.findOne({ clientId: userInfo.clientId })
|
const accessToken = generateAccessToken(userInfo)
|
||||||
if (!client) throw new Error('Invalid clientId.')
|
const refreshToken = generateRefreshToken(userInfo)
|
||||||
|
|
||||||
const accessToken = generateAccessToken(
|
|
||||||
userInfo,
|
|
||||||
client.accessTokenExpiration
|
|
||||||
)
|
|
||||||
const refreshToken = generateRefreshToken(
|
|
||||||
userInfo,
|
|
||||||
client.refreshTokenExpiration
|
|
||||||
)
|
|
||||||
|
|
||||||
await saveTokensInDB(
|
await saveTokensInDB(
|
||||||
userInfo.userId,
|
userInfo.userId,
|
||||||
@@ -153,38 +139,30 @@ const logout = async (userInfo: InfoJWT) => {
|
|||||||
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePassword = async (
|
interface AuthorizePayload {
|
||||||
req: express.Request,
|
/**
|
||||||
data: UpdatePasswordPayload
|
* Username for user
|
||||||
) => {
|
* @example "secretuser"
|
||||||
const { currentPassword, newPassword } = data
|
*/
|
||||||
const userId = req.user?.userId
|
username: string
|
||||||
const dbUser = await User.findOne({ id: userId })
|
/**
|
||||||
|
* Password for user
|
||||||
|
* @example "secretpassword"
|
||||||
|
*/
|
||||||
|
password: string
|
||||||
|
/**
|
||||||
|
* Client ID
|
||||||
|
* @example "clientID1"
|
||||||
|
*/
|
||||||
|
clientId: string
|
||||||
|
}
|
||||||
|
|
||||||
if (!dbUser)
|
interface AuthorizeResponse {
|
||||||
throw {
|
/**
|
||||||
code: 404,
|
* Authorization code
|
||||||
message: `User not found!`
|
* @example "someRandomCryptoString"
|
||||||
}
|
*/
|
||||||
|
code: string
|
||||||
if (dbUser?.authProvider) {
|
|
||||||
throw {
|
|
||||||
code: 405,
|
|
||||||
message:
|
|
||||||
'Can not update password of user that is created by an external auth provider.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validPass = dbUser.comparePassword(currentPassword)
|
|
||||||
if (!validPass)
|
|
||||||
throw {
|
|
||||||
code: 403,
|
|
||||||
message: `Invalid current password!`
|
|
||||||
}
|
|
||||||
|
|
||||||
dbUser.password = User.hashPassword(newPassword)
|
|
||||||
dbUser.needsToUpdatePassword = false
|
|
||||||
await dbUser.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TokenPayload {
|
interface TokenPayload {
|
||||||
@@ -213,31 +191,17 @@ interface TokenResponse {
|
|||||||
refreshToken: string
|
refreshToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdatePasswordPayload {
|
|
||||||
/**
|
|
||||||
* Current Password
|
|
||||||
* @example "currentPasswordString"
|
|
||||||
*/
|
|
||||||
currentPassword: string
|
|
||||||
/**
|
|
||||||
* New Password
|
|
||||||
* @example "newPassword"
|
|
||||||
*/
|
|
||||||
newPassword: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyAuthCode = async (
|
const verifyAuthCode = async (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
code: string
|
code: string
|
||||||
): Promise<InfoJWT | undefined> => {
|
): Promise<InfoJWT | undefined> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
|
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
|
||||||
if (err) return resolve(undefined)
|
if (err) return resolve(undefined)
|
||||||
|
|
||||||
const payload = data as InfoJWT
|
|
||||||
const clientInfo: InfoJWT = {
|
const clientInfo: InfoJWT = {
|
||||||
clientId: payload?.clientId,
|
clientId: data?.clientId,
|
||||||
userId: payload?.userId
|
userId: data?.userId
|
||||||
}
|
}
|
||||||
if (clientInfo.clientId === clientId) {
|
if (clientInfo.clientId === clientId) {
|
||||||
return resolve(clientInfo)
|
return resolve(clientInfo)
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { Security, Route, Tags, Get, Post, Example } from 'tsoa'
|
|
||||||
|
|
||||||
import { LDAPClient, LDAPUser, LDAPGroup, AuthProviderType } from '../utils'
|
|
||||||
import { randomBytes } from 'crypto'
|
|
||||||
import User from '../model/User'
|
|
||||||
import Group from '../model/Group'
|
|
||||||
import Permission from '../model/Permission'
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
|
||||||
@Route('SASjsApi/authConfig')
|
|
||||||
@Tags('Auth_Config')
|
|
||||||
export class AuthConfigController {
|
|
||||||
/**
|
|
||||||
* @summary Gives the detail of Auth Mechanism.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example({
|
|
||||||
ldap: {
|
|
||||||
LDAP_URL: 'ldaps://my.ldap.server:636',
|
|
||||||
LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron',
|
|
||||||
LDAP_BIND_PASSWORD: 'secret',
|
|
||||||
LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron',
|
|
||||||
LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Get('/')
|
|
||||||
public getDetail() {
|
|
||||||
return getAuthConfigDetail()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example({
|
|
||||||
users: 5,
|
|
||||||
groups: 3
|
|
||||||
})
|
|
||||||
@Post('/synchroniseWithLDAP')
|
|
||||||
public async synchroniseWithLDAP() {
|
|
||||||
return synchroniseWithLDAP()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const synchroniseWithLDAP = async () => {
|
|
||||||
process.logger.info('Syncing LDAP with internal DB')
|
|
||||||
|
|
||||||
const permissions = await Permission.get({})
|
|
||||||
await Permission.deleteMany()
|
|
||||||
await User.deleteMany({ authProvider: AuthProviderType.LDAP })
|
|
||||||
await Group.deleteMany({ authProvider: AuthProviderType.LDAP })
|
|
||||||
|
|
||||||
const ldapClient = await LDAPClient.init()
|
|
||||||
|
|
||||||
process.logger.info('fetching LDAP users')
|
|
||||||
const users = await ldapClient.getAllLDAPUsers()
|
|
||||||
|
|
||||||
process.logger.info('inserting LDAP users to DB')
|
|
||||||
|
|
||||||
const existingUsers: string[] = []
|
|
||||||
const importedUsers: LDAPUser[] = []
|
|
||||||
|
|
||||||
for (const user of users) {
|
|
||||||
const usernameExists = await User.findOne({ username: user.username })
|
|
||||||
if (usernameExists) {
|
|
||||||
existingUsers.push(user.username)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashPassword = User.hashPassword(randomBytes(64).toString('hex'))
|
|
||||||
|
|
||||||
await User.create({
|
|
||||||
displayName: user.displayName,
|
|
||||||
username: user.username,
|
|
||||||
password: hashPassword,
|
|
||||||
authProvider: AuthProviderType.LDAP,
|
|
||||||
needsToUpdatePassword: false
|
|
||||||
})
|
|
||||||
|
|
||||||
importedUsers.push(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingUsers.length > 0) {
|
|
||||||
process.logger.info(
|
|
||||||
'Failed to insert following users as they already exist in DB:'
|
|
||||||
)
|
|
||||||
existingUsers.forEach((user) => process.logger.log(`* ${user}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
process.logger.info('fetching LDAP groups')
|
|
||||||
const groups = await ldapClient.getAllLDAPGroups()
|
|
||||||
|
|
||||||
process.logger.info('inserting LDAP groups to DB')
|
|
||||||
|
|
||||||
const existingGroups: string[] = []
|
|
||||||
const importedGroups: LDAPGroup[] = []
|
|
||||||
|
|
||||||
for (const group of groups) {
|
|
||||||
const groupExists = await Group.findOne({ name: group.name })
|
|
||||||
if (groupExists) {
|
|
||||||
existingGroups.push(group.name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
await Group.create({
|
|
||||||
name: group.name,
|
|
||||||
authProvider: AuthProviderType.LDAP
|
|
||||||
})
|
|
||||||
|
|
||||||
importedGroups.push(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingGroups.length > 0) {
|
|
||||||
process.logger.info(
|
|
||||||
'Failed to insert following groups as they already exist in DB:'
|
|
||||||
)
|
|
||||||
existingGroups.forEach((group) => process.logger.log(`* ${group}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
process.logger.info('associating users and groups')
|
|
||||||
|
|
||||||
for (const group of importedGroups) {
|
|
||||||
const dbGroup = await Group.findOne({ name: group.name })
|
|
||||||
if (dbGroup) {
|
|
||||||
for (const member of group.members) {
|
|
||||||
const user = importedUsers.find((user) => user.uid === member)
|
|
||||||
if (user) {
|
|
||||||
const dbUser = await User.findOne({ username: user.username })
|
|
||||||
if (dbUser) await dbGroup.addUser(dbUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.logger.info('setting permissions')
|
|
||||||
|
|
||||||
for (const permission of permissions) {
|
|
||||||
const newPermission = new Permission({
|
|
||||||
path: permission.path,
|
|
||||||
type: permission.type,
|
|
||||||
setting: permission.setting
|
|
||||||
})
|
|
||||||
|
|
||||||
if (permission.user) {
|
|
||||||
const dbUser = await User.findOne({ username: permission.user.username })
|
|
||||||
if (dbUser) newPermission.user = dbUser._id
|
|
||||||
} else if (permission.group) {
|
|
||||||
const dbGroup = await Group.findOne({ name: permission.group.name })
|
|
||||||
if (dbGroup) newPermission.group = dbGroup._id
|
|
||||||
}
|
|
||||||
await newPermission.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
process.logger.info('LDAP synchronization completed!')
|
|
||||||
|
|
||||||
return {
|
|
||||||
userCount: importedUsers.length,
|
|
||||||
groupCount: importedGroups.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAuthConfigDetail = () => {
|
|
||||||
const { AUTH_PROVIDERS } = process.env
|
|
||||||
|
|
||||||
const returnObj: any = {}
|
|
||||||
|
|
||||||
if (AUTH_PROVIDERS === AuthProviderType.LDAP) {
|
|
||||||
const {
|
|
||||||
LDAP_URL,
|
|
||||||
LDAP_BIND_DN,
|
|
||||||
LDAP_BIND_PASSWORD,
|
|
||||||
LDAP_USERS_BASE_DN,
|
|
||||||
LDAP_GROUPS_BASE_DN
|
|
||||||
} = process.env
|
|
||||||
|
|
||||||
returnObj.ldap = {
|
|
||||||
LDAP_URL: LDAP_URL ?? '',
|
|
||||||
LDAP_BIND_DN: LDAP_BIND_DN ?? '',
|
|
||||||
LDAP_BIND_PASSWORD: LDAP_BIND_PASSWORD ?? '',
|
|
||||||
LDAP_USERS_BASE_DN: LDAP_USERS_BASE_DN ?? '',
|
|
||||||
LDAP_GROUPS_BASE_DN: LDAP_GROUPS_BASE_DN ?? ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return returnObj
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,18 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body, Get } from 'tsoa'
|
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
|
||||||
|
|
||||||
import Client, {
|
import Client, { ClientPayload } from '../model/Client'
|
||||||
ClientPayload,
|
|
||||||
NUMBER_OF_SECONDS_IN_A_DAY
|
|
||||||
} from '../model/Client'
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/client')
|
@Route('SASjsApi/client')
|
||||||
@Tags('Client')
|
@Tags('Client')
|
||||||
export class ClientController {
|
export class ClientController {
|
||||||
/**
|
/**
|
||||||
* @summary Admin only task. Create client with the following attributes:
|
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
|
||||||
* ClientId,
|
|
||||||
* ClientSecret,
|
|
||||||
* accessTokenExpiration (optional),
|
|
||||||
* refreshTokenExpiration (optional)
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<ClientPayload>({
|
@Example<ClientPayload>({
|
||||||
clientId: 'someFormattedClientID1234',
|
clientId: 'someFormattedClientID1234',
|
||||||
clientSecret: 'someRandomCryptoString',
|
clientSecret: 'someRandomCryptoString'
|
||||||
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
|
||||||
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
|
||||||
})
|
})
|
||||||
@Post('/')
|
@Post('/')
|
||||||
public async createClient(
|
public async createClient(
|
||||||
@@ -29,37 +20,10 @@ export class ClientController {
|
|||||||
): Promise<ClientPayload> {
|
): Promise<ClientPayload> {
|
||||||
return createClient(body)
|
return createClient(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Admin only task. Returns the list of all the clients
|
|
||||||
*/
|
|
||||||
@Example<ClientPayload[]>([
|
|
||||||
{
|
|
||||||
clientId: 'someClientID1234',
|
|
||||||
clientSecret: 'someRandomCryptoString',
|
|
||||||
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
|
||||||
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
|
||||||
},
|
|
||||||
{
|
|
||||||
clientId: 'someOtherClientID',
|
|
||||||
clientSecret: 'someOtherRandomCryptoString',
|
|
||||||
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
|
||||||
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
|
||||||
}
|
|
||||||
])
|
|
||||||
@Get('/')
|
|
||||||
public async getAllClients(): Promise<ClientPayload[]> {
|
|
||||||
return getAllClients()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
const createClient = async (data: any): Promise<ClientPayload> => {
|
||||||
const {
|
const { clientId, clientSecret } = data
|
||||||
clientId,
|
|
||||||
clientSecret,
|
|
||||||
accessTokenExpiration,
|
|
||||||
refreshTokenExpiration
|
|
||||||
} = data
|
|
||||||
|
|
||||||
// Checking if client is already in the database
|
// Checking if client is already in the database
|
||||||
const clientExist = await Client.findOne({ clientId })
|
const clientExist = await Client.findOne({ clientId })
|
||||||
@@ -68,27 +32,13 @@ const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
|||||||
// Create a new client
|
// Create a new client
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret
|
||||||
accessTokenExpiration,
|
|
||||||
refreshTokenExpiration
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const savedClient = await client.save()
|
const savedClient = await client.save()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clientId: savedClient.clientId,
|
clientId: savedClient.clientId,
|
||||||
clientSecret: savedClient.clientSecret,
|
clientSecret: savedClient.clientSecret
|
||||||
accessTokenExpiration: savedClient.accessTokenExpiration,
|
|
||||||
refreshTokenExpiration: savedClient.refreshTokenExpiration
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllClients = async (): Promise<ClientPayload[]> => {
|
|
||||||
return Client.find({}).select({
|
|
||||||
_id: 0,
|
|
||||||
clientId: 1,
|
|
||||||
clientSecret: 1,
|
|
||||||
accessTokenExpiration: 1,
|
|
||||||
refreshTokenExpiration: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,114 +1,52 @@
|
|||||||
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 { ExecutionController, getSessionController } from './internal'
|
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||||
import {
|
import { PreProgramVars } from '../types'
|
||||||
getPreProgramVariables,
|
import { ExecuteReturnJsonResponse } from '.'
|
||||||
getUserAutoExec,
|
import { parseLogToArray } from '../utils'
|
||||||
ModeType,
|
|
||||||
RunTimeType
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
interface ExecuteCodePayload {
|
interface ExecuteSASCodePayload {
|
||||||
/**
|
/**
|
||||||
* The code to be executed
|
* Code of SAS program
|
||||||
* @example "* Your Code HERE;"
|
* @example "* SAS Code HERE;"
|
||||||
*/
|
*/
|
||||||
code: string
|
code: string
|
||||||
/**
|
|
||||||
* The runtime for the code - eg SAS, JS, PY or R
|
|
||||||
* @example "js"
|
|
||||||
*/
|
|
||||||
runTime: RunTimeType
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TriggerCodePayload {
|
|
||||||
/**
|
|
||||||
* The code to be executed
|
|
||||||
* @example "* Your Code HERE;"
|
|
||||||
*/
|
|
||||||
code: string
|
|
||||||
/**
|
|
||||||
* The runtime for the code - eg SAS, JS, PY or R
|
|
||||||
* @example "sas"
|
|
||||||
*/
|
|
||||||
runTime: RunTimeType
|
|
||||||
/**
|
|
||||||
* Amount of minutes after the completion of the job when the session must be
|
|
||||||
* destroyed.
|
|
||||||
* @example 15
|
|
||||||
*/
|
|
||||||
expiresAfterMins?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TriggerCodeResponse {
|
|
||||||
/**
|
|
||||||
* `sessionId` is the ID of the session and the name of the temporary folder
|
|
||||||
* used to store code outputs.<br><br>
|
|
||||||
* For SAS, this would be the location of the SASWORK folder.<br><br>
|
|
||||||
* `sessionId` can be used to poll session state using the
|
|
||||||
* GET /SASjsApi/session/{sessionId}/state endpoint.
|
|
||||||
* @example "20241028074744-54132-1730101664824"
|
|
||||||
*/
|
|
||||||
sessionId: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/code')
|
@Route('SASjsApi/code')
|
||||||
@Tags('Code')
|
@Tags('CODE')
|
||||||
export class CodeController {
|
export class CodeController {
|
||||||
/**
|
/**
|
||||||
* Execute Code on the Specified Runtime
|
* Execute SAS code.
|
||||||
* @summary Run Code and Return Webout Content, Log and Print output
|
* @summary Run SAS Code and returns log
|
||||||
* The order of returned parts of the payload is:
|
|
||||||
* 1. Webout (if present)
|
|
||||||
* 2. Logs UUID (used as separator)
|
|
||||||
* 3. Log
|
|
||||||
* 4. Logs UUID (used as separator)
|
|
||||||
* 5. Print (if present and if the runtime is SAS)
|
|
||||||
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
|
|
||||||
*/
|
*/
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executeCode(
|
public async executeSASCode(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body: ExecuteCodePayload
|
@Body() body: ExecuteSASCodePayload
|
||||||
): Promise<string | Buffer> {
|
): Promise<ExecuteReturnJsonResponse> {
|
||||||
return executeCode(request, body)
|
return executeSASCode(request, body)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger Code on the Specified Runtime
|
|
||||||
* @summary Triggers code and returns SessionId immediately - does not wait for job completion
|
|
||||||
*/
|
|
||||||
@Post('/trigger')
|
|
||||||
public async triggerCode(
|
|
||||||
@Request() request: express.Request,
|
|
||||||
@Body() body: TriggerCodePayload
|
|
||||||
): Promise<TriggerCodeResponse> {
|
|
||||||
return triggerCode(request, body)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeCode = async (
|
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||||
req: express.Request,
|
|
||||||
{ code, runTime }: ExecuteCodePayload
|
|
||||||
) => {
|
|
||||||
const { user } = req
|
|
||||||
const userAutoExec =
|
|
||||||
process.env.MODE === ModeType.Server
|
|
||||||
? user?.autoExec
|
|
||||||
: await getUserAutoExec()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { result } = await new ExecutionController().executeProgram({
|
const { webout, log, httpHeaders } =
|
||||||
program: code,
|
(await new ExecutionController().executeProgram(
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
code,
|
||||||
vars: { ...req.query, _debug: 131 },
|
getPreProgramVariables(req),
|
||||||
otherArgs: { userAutoExec },
|
{ ...req.query, _debug: 131 },
|
||||||
runTime: runTime,
|
undefined,
|
||||||
includePrintOutput: true
|
true
|
||||||
})
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
return result
|
return {
|
||||||
|
status: 'success',
|
||||||
|
_webout: webout as string,
|
||||||
|
log: parseLogToArray(log),
|
||||||
|
httpHeaders
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
@@ -119,48 +57,15 @@ const executeCode = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerCode = async (
|
const getPreProgramVariables = (req: any): PreProgramVars => {
|
||||||
req: express.Request,
|
const host = req.get('host')
|
||||||
{ code, runTime, expiresAfterMins }: TriggerCodePayload
|
const protocol = req.protocol + '://'
|
||||||
): Promise<TriggerCodeResponse> => {
|
const { user, accessToken } = req
|
||||||
const { user } = req
|
return {
|
||||||
const userAutoExec =
|
username: user.username,
|
||||||
process.env.MODE === ModeType.Server
|
userId: user.userId,
|
||||||
? user?.autoExec
|
displayName: user.displayName,
|
||||||
: await getUserAutoExec()
|
serverUrl: protocol + host,
|
||||||
|
accessToken
|
||||||
// get session controller based on runTime
|
|
||||||
const sessionController = getSessionController(runTime)
|
|
||||||
|
|
||||||
// get session
|
|
||||||
const session = await sessionController.getSession()
|
|
||||||
|
|
||||||
// add expiresAfterMins to session if provided
|
|
||||||
if (expiresAfterMins) {
|
|
||||||
// expiresAfterMins.used is set initially to false
|
|
||||||
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// call executeProgram method of ExecutionController without awaiting
|
|
||||||
new ExecutionController().executeProgram({
|
|
||||||
program: code,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars: { ...req.query, _debug: 131 },
|
|
||||||
otherArgs: { userAutoExec },
|
|
||||||
runTime: runTime,
|
|
||||||
includePrintOutput: true,
|
|
||||||
session // session is provided
|
|
||||||
})
|
|
||||||
|
|
||||||
// return session id
|
|
||||||
return { sessionId: session.id }
|
|
||||||
} catch (err: any) {
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'Job execution failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
moveFile,
|
moveFile,
|
||||||
createFolder,
|
createFolder,
|
||||||
deleteFile as deleteFileOnSystem,
|
deleteFile as deleteFileOnSystem,
|
||||||
deleteFolder as deleteFolderOnSystem,
|
|
||||||
folderExists,
|
folderExists,
|
||||||
listFilesInFolder,
|
listFilesInFolder,
|
||||||
listSubFoldersInFolder,
|
listSubFoldersInFolder,
|
||||||
@@ -33,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
|
||||||
@@ -59,32 +58,11 @@ interface GetFileTreeResponse {
|
|||||||
tree: TreeNode
|
tree: TreeNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileFolderResponse {
|
interface UpdateFileResponse {
|
||||||
status: string
|
status: string
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddFolderPayload {
|
|
||||||
/**
|
|
||||||
* Location of folder
|
|
||||||
* @example "/Public/someFolder"
|
|
||||||
*/
|
|
||||||
folderPath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RenamePayload {
|
|
||||||
/**
|
|
||||||
* Old path of file/folder
|
|
||||||
* @example "/Public/someFolder"
|
|
||||||
*/
|
|
||||||
oldPath: string
|
|
||||||
/**
|
|
||||||
* New path of file/folder
|
|
||||||
* @example "/Public/newFolder"
|
|
||||||
*/
|
|
||||||
newPath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileTreeExample = getTreeExample()
|
const fileTreeExample = getTreeExample()
|
||||||
|
|
||||||
const successDeployResponse: DeployResponse = {
|
const successDeployResponse: DeployResponse = {
|
||||||
@@ -118,12 +96,7 @@ export class DriveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts JSON file and zipped compressed JSON file as well.
|
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
|
||||||
* Compressed file should only contain one JSON file and should have same name
|
|
||||||
* as of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip
|
|
||||||
* Any other file or JSON file in zipped will be ignored!
|
|
||||||
*
|
|
||||||
* @summary Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<DeployResponse>(successDeployResponse)
|
@Example<DeployResponse>(successDeployResponse)
|
||||||
@@ -165,7 +138,7 @@ export class DriveController {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @summary Delete file from SASjs Drive
|
* @summary Delete file from SASjs Drive
|
||||||
* @query _filePath Location of file
|
* @query _filePath Location of SAS program
|
||||||
* @example _filePath "/Public/somefolder/some.file"
|
* @example _filePath "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Delete('/file')
|
@Delete('/file')
|
||||||
@@ -173,31 +146,20 @@ export class DriveController {
|
|||||||
return deleteFile(_filePath)
|
return deleteFile(_filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @summary Delete folder from SASjs Drive
|
|
||||||
* @query _folderPath Location of folder
|
|
||||||
* @example _folderPath "/Public/somefolder/"
|
|
||||||
*/
|
|
||||||
@Delete('/folder')
|
|
||||||
public async deleteFolder(@Query() _folderPath: string) {
|
|
||||||
return deleteFolder(_folderPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
* But it's required to provide else API will respond with Bad Request.
|
* But it's required to provide else API will respond with Bad Request.
|
||||||
*
|
*
|
||||||
* @summary Create a file in SASjs Drive
|
* @summary Create a file in SASjs Drive
|
||||||
* @param _filePath Location of file
|
* @param _filePath Location of SAS program
|
||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<FileFolderResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<FileFolderResponse>(403, 'File already exists', {
|
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -206,28 +168,10 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<FileFolderResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return saveFile((_filePath ?? filePath)!, file)
|
return saveFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Create an empty folder in SASjs Drive
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<FileFolderResponse>({
|
|
||||||
status: 'success'
|
|
||||||
})
|
|
||||||
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
|
||||||
status: 'failure',
|
|
||||||
message: 'Add folder request failed.'
|
|
||||||
})
|
|
||||||
@Post('/folder')
|
|
||||||
public async addFolder(
|
|
||||||
@Body() body: AddFolderPayload
|
|
||||||
): Promise<FileFolderResponse> {
|
|
||||||
return addFolder(body.folderPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
@@ -238,10 +182,10 @@ export class DriveController {
|
|||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<FileFolderResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<FileFolderResponse>(403, `File doesn't exist`, {
|
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -250,28 +194,10 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<FileFolderResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return updateFile((_filePath ?? filePath)!, file)
|
return updateFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Renames a file/folder in SASjs Drive
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<FileFolderResponse>({
|
|
||||||
status: 'success'
|
|
||||||
})
|
|
||||||
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
|
||||||
status: 'failure',
|
|
||||||
message: 'rename request failed.'
|
|
||||||
})
|
|
||||||
@Post('/rename')
|
|
||||||
public async rename(
|
|
||||||
@Body() body: RenamePayload
|
|
||||||
): Promise<FileFolderResponse> {
|
|
||||||
return rename(body.oldPath, body.newPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Fetch file tree within SASjs Drive.
|
* @summary Fetch file tree within SASjs Drive.
|
||||||
*
|
*
|
||||||
@@ -288,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)) {
|
||||||
@@ -312,62 +238,47 @@ 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)) {
|
||||||
throw {
|
throw new Error('Cannot get file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't get file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull)))
|
if (!(await fileExists(filePathFull))) {
|
||||||
throw {
|
throw new Error("File doesn't exist.")
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `File doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
const extension = path.extname(filePathFull).toLowerCase()
|
const extension = path.extname(filePathFull).toLowerCase()
|
||||||
if (extension === '.sas') {
|
if (extension === '.sas') {
|
||||||
req.res?.setHeader('Content-type', 'text/plain')
|
req.res?.setHeader('Content-type', 'text/plain')
|
||||||
}
|
}
|
||||||
|
|
||||||
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
|
req.res?.sendFile(path.resolve(filePathFull))
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
throw {
|
throw new Error('Cannot get folder outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't get folder outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await folderExists(folderPathFull)))
|
if (!(await folderExists(folderPathFull))) {
|
||||||
throw {
|
throw new Error("Folder doesn't exist.")
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `Folder doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await isFolder(folderPathFull)))
|
if (!(await isFolder(folderPathFull))) {
|
||||||
throw {
|
throw new Error('Not a Folder.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: 'Not a Folder.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const files: string[] = await listFilesInFolder(folderPathFull)
|
const files: string[] = await listFilesInFolder(folderPathFull)
|
||||||
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
||||||
@@ -380,80 +291,42 @@ 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)) {
|
||||||
throw {
|
throw new Error('Cannot delete file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't delete file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull)))
|
if (!(await fileExists(filePathFull))) {
|
||||||
throw {
|
throw new Error('File does not exist.')
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `File doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteFileOnSystem(filePathFull)
|
await deleteFileOnSystem(filePathFull)
|
||||||
|
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteFolder = async (folderPath: string) => {
|
|
||||||
const driveFolderPath = getFilesFolder()
|
|
||||||
|
|
||||||
const folderPathFull = path
|
|
||||||
.join(getFilesFolder(), folderPath)
|
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
if (!folderPathFull.includes(driveFolderPath))
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't delete folder outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await folderExists(folderPathFull)))
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: `Folder doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteFolderOnSystem(folderPathFull)
|
|
||||||
|
|
||||||
return { status: 'success' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveFile = async (
|
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)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath))
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
throw {
|
throw new Error('Cannot put file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't put file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await fileExists(filePathFull))
|
if (await fileExists(filePathFull)) {
|
||||||
throw {
|
throw new Error('File already exists.')
|
||||||
code: 409,
|
}
|
||||||
status: 'Conflict',
|
|
||||||
message: 'File already exists.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const folderPath = path.dirname(filePathFull)
|
const folderPath = path.dirname(filePathFull)
|
||||||
await createFolder(folderPath)
|
await createFolder(folderPath)
|
||||||
@@ -462,111 +335,23 @@ const saveFile = async (
|
|||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const addFolder = async (folderPath: string): Promise<FileFolderResponse> => {
|
|
||||||
const drivePath = getFilesFolder()
|
|
||||||
|
|
||||||
const folderPathFull = path
|
|
||||||
.join(drivePath, folderPath)
|
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
if (!folderPathFull.includes(drivePath))
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't put folder outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await folderExists(folderPathFull))
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message: 'Folder already exists.'
|
|
||||||
}
|
|
||||||
|
|
||||||
await createFolder(folderPathFull)
|
|
||||||
|
|
||||||
return { status: 'success' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const rename = async (
|
|
||||||
oldPath: string,
|
|
||||||
newPath: string
|
|
||||||
): Promise<FileFolderResponse> => {
|
|
||||||
const drivePath = getFilesFolder()
|
|
||||||
|
|
||||||
const oldPathFull = path
|
|
||||||
.join(drivePath, oldPath)
|
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
const newPathFull = path
|
|
||||||
.join(drivePath, newPath)
|
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
if (!oldPathFull.includes(drivePath))
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Old path can't be outside of drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newPathFull.includes(drivePath))
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: `New path can't be outside of drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await isFolder(oldPathFull)) {
|
|
||||||
if (await folderExists(newPathFull))
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message: 'Folder with new name already exists.'
|
|
||||||
}
|
|
||||||
else moveFile(oldPathFull, newPathFull)
|
|
||||||
|
|
||||||
return { status: 'success' }
|
|
||||||
} else if (await fileExists(oldPathFull)) {
|
|
||||||
if (await fileExists(newPathFull))
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message: 'File with new name already exists.'
|
|
||||||
}
|
|
||||||
else moveFile(oldPathFull, newPathFull)
|
|
||||||
return { status: 'success' }
|
|
||||||
}
|
|
||||||
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'No file/folder found for provided path.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateFile = async (
|
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)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath))
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
throw {
|
throw new Error('Cannot modify file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't modify file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull)))
|
if (!(await fileExists(filePathFull))) {
|
||||||
throw {
|
throw new Error(`File doesn't exist.`)
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `File doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
await moveFile(multerFile.path, filePathFull)
|
await moveFile(multerFile.path, filePathFull)
|
||||||
|
|
||||||
|
|||||||
@@ -10,18 +10,17 @@ import {
|
|||||||
Body
|
Body
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
|
|
||||||
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
|
import Group, { GroupPayload } from '../model/Group'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import { AuthProviderType } from '../utils'
|
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
|
|
||||||
export interface GroupResponse {
|
interface GroupResponse {
|
||||||
groupId: number
|
groupId: number
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupDetailsResponse {
|
interface GroupDetailsResponse {
|
||||||
groupId: number
|
groupId: number
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -29,11 +28,6 @@ export interface GroupDetailsResponse {
|
|||||||
users: UserResponse[]
|
users: UserResponse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetGroupBy {
|
|
||||||
groupId?: number
|
|
||||||
name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/group')
|
@Route('SASjsApi/group')
|
||||||
@Tags('Group')
|
@Tags('Group')
|
||||||
@@ -72,18 +66,6 @@ export class GroupController {
|
|||||||
return createGroup(body)
|
return createGroup(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get list of members of a group (userName). All users can request this.
|
|
||||||
* @param name The group's name
|
|
||||||
* @example dcgroup
|
|
||||||
*/
|
|
||||||
@Get('by/groupname/{name}')
|
|
||||||
public async getGroupByGroupName(
|
|
||||||
@Path() name: string
|
|
||||||
): Promise<GroupDetailsResponse> {
|
|
||||||
return getGroup({ name })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Get list of members of a group (userName). All users can request this.
|
* @summary Get list of members of a group (userName). All users can request this.
|
||||||
* @param groupId The group's identifier
|
* @param groupId The group's identifier
|
||||||
@@ -93,7 +75,7 @@ export class GroupController {
|
|||||||
public async getGroup(
|
public async getGroup(
|
||||||
@Path() groupId: number
|
@Path() groupId: number
|
||||||
): Promise<GroupDetailsResponse> {
|
): Promise<GroupDetailsResponse> {
|
||||||
return getGroup({ groupId })
|
return getGroup(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,15 +129,9 @@ export class GroupController {
|
|||||||
*/
|
*/
|
||||||
@Delete('{groupId}')
|
@Delete('{groupId}')
|
||||||
public async deleteGroup(@Path() groupId: number) {
|
public async deleteGroup(@Path() groupId: number) {
|
||||||
const group = await Group.findOne({ groupId })
|
const { deletedCount } = await Group.deleteOne({ groupId })
|
||||||
if (!group)
|
if (deletedCount) return
|
||||||
throw {
|
throw new Error('No Group deleted!')
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Group not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
return await group.remove()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,15 +145,6 @@ const createGroup = async ({
|
|||||||
description,
|
description,
|
||||||
isActive
|
isActive
|
||||||
}: GroupPayload): Promise<GroupDetailsResponse> => {
|
}: GroupPayload): Promise<GroupDetailsResponse> => {
|
||||||
// Checking if user is already in the database
|
|
||||||
const groupnameExist = await Group.findOne({ name })
|
|
||||||
if (groupnameExist)
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message: 'Group name already exists.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = new Group({
|
const group = new Group({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -195,20 +162,15 @@ const createGroup = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
const getGroup = async (groupId: number): Promise<GroupDetailsResponse> => {
|
||||||
const group = (await Group.findOne(
|
const group = (await Group.findOne(
|
||||||
findBy,
|
{ groupId },
|
||||||
'groupId name description isActive users -_id'
|
'groupId name description isActive users -_id'
|
||||||
).populate(
|
).populate(
|
||||||
'users',
|
'users',
|
||||||
'id username displayName isAdmin -_id'
|
'id username displayName -_id'
|
||||||
)) as unknown as GroupDetailsResponse
|
)) as unknown as GroupDetailsResponse
|
||||||
if (!group)
|
if (!group) throw new Error('Group not found.')
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Group not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: group.groupId,
|
groupId: group.groupId,
|
||||||
@@ -237,53 +199,16 @@ const updateUsersListInGroup = async (
|
|||||||
action: 'addUser' | 'removeUser'
|
action: 'addUser' | 'removeUser'
|
||||||
): Promise<GroupDetailsResponse> => {
|
): Promise<GroupDetailsResponse> => {
|
||||||
const group = await Group.findOne({ groupId })
|
const group = await Group.findOne({ groupId })
|
||||||
if (!group)
|
if (!group) throw new Error('Group not found.')
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Group not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.name === PUBLIC_GROUP_NAME)
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.authProvider)
|
|
||||||
throw {
|
|
||||||
code: 405,
|
|
||||||
status: 'Method Not Allowed',
|
|
||||||
message: `Can't add/remove user to group created by external auth provider.`
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findOne({ id: userId })
|
const user = await User.findOne({ id: userId })
|
||||||
if (!user)
|
if (!user) throw new Error('User not found.')
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'User not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.authProvider)
|
const updatedGroup = (action === 'addUser'
|
||||||
throw {
|
? await group.addUser(user._id)
|
||||||
code: 405,
|
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
|
||||||
status: 'Method Not Allowed',
|
|
||||||
message: `Can't add/remove user to group created by external auth provider.`
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedGroup =
|
if (!updatedGroup) throw new Error('Unable to update group')
|
||||||
action === 'addUser'
|
|
||||||
? await group.addUser(user)
|
|
||||||
: await group.removeUser(user)
|
|
||||||
|
|
||||||
if (!updatedGroup)
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: 'Unable to update group.'
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: updatedGroup.groupId,
|
groupId: updatedGroup.groupId,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
export * from './auth'
|
export * from './auth'
|
||||||
export * from './authConfig'
|
|
||||||
export * from './client'
|
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 './permission'
|
|
||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './web'
|
export * from './info'
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { Route, Tags, Example, Get } from 'tsoa'
|
import { Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { getAuthorizedRoutes } from '../utils'
|
|
||||||
export interface AuthorizedRoutesResponse {
|
|
||||||
paths: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InfoResponse {
|
export interface InfoResponse {
|
||||||
mode: string
|
mode: string
|
||||||
cors: string
|
cors: string
|
||||||
whiteList: string[]
|
whiteList: string[]
|
||||||
protocol: string
|
protocol: string
|
||||||
runTimes: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Route('SASjsApi/info')
|
@Route('SASjsApi/info')
|
||||||
@@ -23,35 +18,19 @@ export class InfoController {
|
|||||||
mode: 'desktop',
|
mode: 'desktop',
|
||||||
cors: 'enable',
|
cors: 'enable',
|
||||||
whiteList: ['http://example.com', 'http://example2.com'],
|
whiteList: ['http://example.com', 'http://example2.com'],
|
||||||
protocol: 'http',
|
protocol: 'http'
|
||||||
runTimes: ['sas', 'js']
|
|
||||||
})
|
})
|
||||||
@Get('/')
|
@Get('/')
|
||||||
public info(): InfoResponse {
|
public info(): InfoResponse {
|
||||||
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'
|
||||||
runTimes: process.runTimes
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<AuthorizedRoutesResponse>({
|
|
||||||
paths: ['/AppStream', '/SASjsApi/stp/execute']
|
|
||||||
})
|
|
||||||
@Get('/authorizedRoutes')
|
|
||||||
public authorizedRoutes(): AuthorizedRoutesResponse {
|
|
||||||
const response = {
|
|
||||||
paths: getAuthorizedRoutes()
|
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { getSessionController, processProgram } from './'
|
import { getSessionController } from './'
|
||||||
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
|
import {
|
||||||
import { PreProgramVars, Session, TreeNode, SessionState } from '../../types'
|
readFile,
|
||||||
|
fileExists,
|
||||||
|
createFile,
|
||||||
|
moveFile,
|
||||||
|
readFileBinary
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||||
import {
|
import {
|
||||||
extractHeaders,
|
extractHeaders,
|
||||||
getFilesFolder,
|
generateFileUploadSasCode,
|
||||||
|
getTmpFilesFolderPath,
|
||||||
|
getTmpMacrosPath,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
isDebugOn
|
||||||
RunTimeType
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
export interface ExecutionVars {
|
export interface ExecutionVars {
|
||||||
@@ -20,132 +27,162 @@ export interface ExecuteReturnRaw {
|
|||||||
result: string | Buffer
|
result: string | Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExecuteFileParams {
|
export interface ExecuteReturnJson {
|
||||||
programPath: string
|
httpHeaders: HTTPHeaders
|
||||||
preProgramVariables: PreProgramVars
|
webout: string | Buffer
|
||||||
vars: ExecutionVars
|
log?: string
|
||||||
otherArgs?: any
|
|
||||||
returnJson?: boolean
|
|
||||||
session?: Session
|
|
||||||
runTime: RunTimeType
|
|
||||||
forceStringResult?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
|
||||||
program: string
|
|
||||||
includePrintOutput?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutionController {
|
export class ExecutionController {
|
||||||
async executeFile({
|
async executeFile(
|
||||||
programPath,
|
programPath: string,
|
||||||
preProgramVariables,
|
preProgramVariables: PreProgramVars,
|
||||||
vars,
|
vars: ExecutionVars,
|
||||||
otherArgs,
|
otherArgs?: any,
|
||||||
returnJson,
|
returnJson?: boolean,
|
||||||
session,
|
session?: Session
|
||||||
runTime,
|
) {
|
||||||
forceStringResult
|
if (!(await fileExists(programPath)))
|
||||||
}: ExecuteFileParams) {
|
throw 'ExecutionController: SAS file does not exist.'
|
||||||
|
|
||||||
const program = await readFile(programPath)
|
const program = await readFile(programPath)
|
||||||
|
|
||||||
return this.executeProgram({
|
return this.executeProgram(
|
||||||
program,
|
program,
|
||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
returnJson,
|
||||||
session,
|
session
|
||||||
runTime,
|
)
|
||||||
forceStringResult
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeProgram({
|
async executeProgram(
|
||||||
program,
|
program: string,
|
||||||
preProgramVariables,
|
preProgramVariables: PreProgramVars,
|
||||||
vars,
|
vars: ExecutionVars,
|
||||||
otherArgs,
|
otherArgs?: any,
|
||||||
session: sessionByFileUpload,
|
returnJson?: boolean,
|
||||||
runTime,
|
sessionByFileUpload?: Session
|
||||||
forceStringResult,
|
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||||
includePrintOutput
|
const sessionController = getSessionController()
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
|
||||||
const sessionController = getSessionController(runTime)
|
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
sessionByFileUpload ?? (await sessionController.getSession())
|
sessionByFileUpload ?? (await sessionController.getSession())
|
||||||
session.state = SessionState.running
|
session.inUse = true
|
||||||
|
session.consumed = true
|
||||||
|
|
||||||
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'
|
||||||
)
|
)
|
||||||
|
|
||||||
await processProgram(
|
const varStatments = Object.keys(vars).reduce(
|
||||||
program,
|
(computed: string, key: string) =>
|
||||||
preProgramVariables,
|
`${computed}%let ${key}=${vars[key]};\n`,
|
||||||
vars,
|
''
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
headersPath,
|
|
||||||
tokenFile,
|
|
||||||
runTime,
|
|
||||||
logPath,
|
|
||||||
otherArgs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const preProgramVarStatments = `
|
||||||
|
%let _sasjs_tokenfile=${tokenFile};
|
||||||
|
%let _sasjs_username=${preProgramVariables?.username};
|
||||||
|
%let _sasjs_userid=${preProgramVariables?.userId};
|
||||||
|
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
||||||
|
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||||
|
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||||
|
%let _metaperson=&_sasjs_displayname;
|
||||||
|
%let _metauser=&_sasjs_username;
|
||||||
|
%let sasjsprocessmode=Stored Program;
|
||||||
|
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
||||||
|
|
||||||
|
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
||||||
|
%macro _sasjs_server_init();
|
||||||
|
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
|
||||||
|
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
||||||
|
%mend;
|
||||||
|
%_sasjs_server_init()
|
||||||
|
`
|
||||||
|
|
||||||
|
program = `
|
||||||
|
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
||||||
|
|
||||||
|
/* runtime vars */
|
||||||
|
${varStatments}
|
||||||
|
filename _webout "${weboutPath}" mod;
|
||||||
|
|
||||||
|
/* dynamic user-provided vars */
|
||||||
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
/* actual job code */
|
||||||
|
${program}`
|
||||||
|
|
||||||
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
|
if (otherArgs?.filesNamesMap) {
|
||||||
|
const uploadSasCode = await generateFileUploadSasCode(
|
||||||
|
otherArgs.filesNamesMap,
|
||||||
|
session.path
|
||||||
|
)
|
||||||
|
|
||||||
|
//If sas code for the file is generated it will be appended to the top of sasCode
|
||||||
|
if (uploadSasCode.length > 0) {
|
||||||
|
program = `${uploadSasCode}` + program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const codePath = path.join(session.path, 'code.sas')
|
||||||
|
|
||||||
|
// Creating this file in a RUNNING session will break out
|
||||||
|
// the autoexec loop and actually execute the program
|
||||||
|
// but - given it will take several milliseconds to create
|
||||||
|
// (which can mean SAS trying to run a partial program, or
|
||||||
|
// failing due to file lock) we first create the file THEN
|
||||||
|
// we rename it.
|
||||||
|
await createFile(codePath + '.bkp', program)
|
||||||
|
await moveFile(codePath + '.bkp', codePath)
|
||||||
|
|
||||||
|
// we now need to poll the session status
|
||||||
|
while (!session.completed) {
|
||||||
|
await delay(50)
|
||||||
|
}
|
||||||
|
|
||||||
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
||||||
const headersContent = (await fileExists(headersPath))
|
const headersContent = (await fileExists(headersPath))
|
||||||
? await readFile(headersPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
|
const fileResponse: boolean =
|
||||||
if (isDebugOn(vars)) {
|
httpHeaders.hasOwnProperty('content-type') &&
|
||||||
httpHeaders['content-type'] = 'text/plain'
|
!returnJson && // not a POST Request
|
||||||
}
|
!isDebugOn(vars) // Debug is not enabled
|
||||||
|
|
||||||
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse && !forceStringResult
|
? fileResponse
|
||||||
? await readFileBinary(weboutPath)
|
? await readFileBinary(weboutPath)
|
||||||
: await readFile(weboutPath)
|
: await readFile(weboutPath)
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
// it should be deleted by scheduleSessionDestroy
|
// it should be deleted by scheduleSessionDestroy
|
||||||
session.state = SessionState.completed
|
session.inUse = false
|
||||||
|
|
||||||
const resultParts = []
|
if (returnJson) {
|
||||||
|
return {
|
||||||
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty
|
httpHeaders,
|
||||||
if (webout && webout.length !== 0) resultParts.push(webout)
|
webout,
|
||||||
|
log: isDebugOn(vars) || session.crashed ? log : undefined
|
||||||
// INFO: log separator wraps the log from the beginning and the end
|
}
|
||||||
resultParts.push(process.logsUUID)
|
|
||||||
resultParts.push(log)
|
|
||||||
resultParts.push(process.logsUUID)
|
|
||||||
|
|
||||||
if (includePrintOutput && runTime === RunTimeType.SAS) {
|
|
||||||
const printOutputPath = path.join(session.path, 'output.lst')
|
|
||||||
const printOutput = (await fileExists(printOutputPath))
|
|
||||||
? await readFile(printOutputPath)
|
|
||||||
: ''
|
|
||||||
|
|
||||||
if (printOutput) resultParts.push(printOutput)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpHeaders,
|
httpHeaders,
|
||||||
result:
|
result:
|
||||||
isDebugOn(vars) || session.failureReason
|
isDebugOn(vars) || session.crashed
|
||||||
? resultParts.join(`\n`)
|
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||||
: webout
|
: webout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,8 +191,7 @@ export class ExecutionController {
|
|||||||
const root: TreeNode = {
|
const root: TreeNode = {
|
||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
absolutePath: getFilesFolder(),
|
absolutePath: getTmpFilesFolderPath(),
|
||||||
isFolder: true,
|
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,22 +201,15 @@ export class ExecutionController {
|
|||||||
const currentNode = stack.pop()
|
const currentNode = stack.pop()
|
||||||
|
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
currentNode.isFolder = fs
|
|
||||||
.statSync(currentNode.absolutePath)
|
|
||||||
.isDirectory()
|
|
||||||
|
|
||||||
const children = fs.readdirSync(currentNode.absolutePath)
|
const children = fs.readdirSync(currentNode.absolutePath)
|
||||||
|
|
||||||
for (let child of children) {
|
for (let child of children) {
|
||||||
const absoluteChildPath = path.join(currentNode.absolutePath, child)
|
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
|
||||||
// relative path will only be used in frontend component
|
|
||||||
// so, no need to convert '/' to platform specific separator
|
|
||||||
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
||||||
const childNode: TreeNode = {
|
const childNode: TreeNode = {
|
||||||
name: child,
|
name: child,
|
||||||
relativePath: relativeChildPath,
|
relativePath: relativeChildPath,
|
||||||
absolutePath: absoluteChildPath,
|
absolutePath: absoluteChildPath,
|
||||||
isFolder: false,
|
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
currentNode.children.push(childNode)
|
currentNode.children.push(childNode)
|
||||||
@@ -195,3 +224,5 @@ export class ExecutionController {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|||||||
@@ -1,17 +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 '.'
|
||||||
import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils'
|
|
||||||
import { SessionState } from '../../types'
|
|
||||||
|
|
||||||
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.sasjsSession?.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, '')}`)
|
||||||
}
|
}
|
||||||
@@ -21,42 +18,16 @@ 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) => {
|
||||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
let session
|
||||||
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
|
||||||
|
|
||||||
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
const sessionController = getSessionController()
|
||||||
|
session = await sessionController.getSession()
|
||||||
|
// marking consumed true, so that it's not available
|
||||||
|
// as readySession for any other request
|
||||||
|
session.consumed = true
|
||||||
|
|
||||||
const programPath = (query?._program ?? body?._program) as string
|
req.sasSession = session
|
||||||
|
|
||||||
let runTime
|
|
||||||
|
|
||||||
try {
|
|
||||||
;({ runTime } = await getRunTimeAndFilePath(programPath))
|
|
||||||
} catch (err: any) {
|
|
||||||
return res.status(400).send({
|
|
||||||
status: 'failure',
|
|
||||||
message: 'Job execution failed',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let sessionController
|
|
||||||
try {
|
|
||||||
sessionController = getSessionController(runTime)
|
|
||||||
} catch (err: any) {
|
|
||||||
return res.status(400).send({
|
|
||||||
status: 'failure',
|
|
||||||
message: err.message,
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await sessionController.getSession()
|
|
||||||
// change session state to 'running', so that it's not available for any other request
|
|
||||||
session.state = SessionState.running
|
|
||||||
|
|
||||||
req.sasjsSession = session
|
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Session, SessionState } from '../../types'
|
import { Session } from '../../types'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import {
|
import {
|
||||||
getPackagesFolder,
|
getTmpSessionsFolderPath,
|
||||||
getSessionsFolder,
|
|
||||||
generateUniqueFileName,
|
generateUniqueFileName,
|
||||||
sysInitCompiledPath,
|
sysInitCompiledPath
|
||||||
RunTimeType
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import {
|
import {
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
@@ -20,41 +18,10 @@ import {
|
|||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
|
|
||||||
export class SessionController {
|
export class SessionController {
|
||||||
protected sessions: Session[] = []
|
private sessions: Session[] = []
|
||||||
|
|
||||||
protected getReadySessions = (): Session[] =>
|
private getReadySessions = (): Session[] =>
|
||||||
this.sessions.filter(
|
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||||
(session: Session) => session.state === SessionState.pending
|
|
||||||
)
|
|
||||||
|
|
||||||
protected async createSession(): Promise<Session> {
|
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
|
||||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
|
||||||
// death time of session is 15 mins from creation
|
|
||||||
const deathTimeStamp = (
|
|
||||||
parseInt(creationTimeStamp) +
|
|
||||||
15 * 60 * 1000 -
|
|
||||||
1000
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
const session: Session = {
|
|
||||||
id: sessionId,
|
|
||||||
state: SessionState.pending,
|
|
||||||
creationTimeStamp,
|
|
||||||
deathTimeStamp,
|
|
||||||
path: sessionFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
|
||||||
|
|
||||||
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
|
|
||||||
|
|
||||||
this.sessions.push(session)
|
|
||||||
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSession() {
|
public async getSession() {
|
||||||
const readySessions = this.getReadySessions()
|
const readySessions = this.getReadySessions()
|
||||||
@@ -68,15 +35,9 @@ export class SessionController {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSessionById(id: string) {
|
private async createSession(): Promise<Session> {
|
||||||
return this.sessions.find((session) => session.id === id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SASSessionController extends SessionController {
|
|
||||||
protected 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
|
||||||
@@ -88,15 +49,15 @@ export class SASSessionController extends SessionController {
|
|||||||
|
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
state: SessionState.initialising,
|
ready: false,
|
||||||
|
inUse: false,
|
||||||
|
consumed: false,
|
||||||
|
completed: false,
|
||||||
creationTimeStamp,
|
creationTimeStamp,
|
||||||
deathTimeStamp,
|
deathTimeStamp,
|
||||||
path: sessionFolder
|
path: sessionFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
|
||||||
await createFile(headersPath, 'content-type: text/html; charset=utf-8\n')
|
|
||||||
|
|
||||||
// we do not want to leave sessions running forever
|
// we do not want to leave sessions running forever
|
||||||
// we clean them up after a predefined period, if unused
|
// we clean them up after a predefined period, if unused
|
||||||
this.scheduleSessionDestroy(session)
|
this.scheduleSessionDestroy(session)
|
||||||
@@ -106,8 +67,7 @@ export class SASSessionController extends SessionController {
|
|||||||
|
|
||||||
// the autoexec file is executed on SAS startup
|
// the autoexec file is executed on SAS startup
|
||||||
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||||
const contentForAutoExec = `filename packages "${getPackagesFolder()}";
|
const contentForAutoExec = `/* compiled systemInit */
|
||||||
/* compiled systemInit */
|
|
||||||
${compiledSystemInitContent}
|
${compiledSystemInitContent}
|
||||||
/* autoexec */
|
/* autoexec */
|
||||||
${autoExecContent}`
|
${autoExecContent}`
|
||||||
@@ -122,9 +82,7 @@ ${autoExecContent}`
|
|||||||
// however we also need a promise so that we can update the
|
// however we also need a promise so that we can update the
|
||||||
// session array to say that it has (eventually) finished.
|
// session array to say that it has (eventually) finished.
|
||||||
|
|
||||||
// Additional windows specific options to avoid the desktop popups.
|
execFilePromise(process.sasLoc, [
|
||||||
|
|
||||||
execFilePromise(process.sasLoc!, [
|
|
||||||
'-SYSIN',
|
'-SYSIN',
|
||||||
codePath,
|
codePath,
|
||||||
'-LOG',
|
'-LOG',
|
||||||
@@ -135,31 +93,16 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nologo' : '',
|
process.platform === 'win32' ? '-nosplash' : ''
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-NOPRNGETLIST' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.state = SessionState.completed
|
session.completed = true
|
||||||
|
console.log('session completed', session)
|
||||||
process.logger.info('session completed', session)
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
session.state = SessionState.failed
|
session.completed = true
|
||||||
|
session.crashed = err.toString()
|
||||||
session.failureReason = err.toString()
|
console.log('session crashed', session.id, session.crashed)
|
||||||
|
|
||||||
process.logger.error(
|
|
||||||
'session crashed',
|
|
||||||
session.id,
|
|
||||||
session.failureReason
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// we have a triggered session - add to array
|
// we have a triggered session - add to array
|
||||||
@@ -176,22 +119,15 @@ ${autoExecContent}`
|
|||||||
const codeFilePath = path.join(session.path, 'code.sas')
|
const codeFilePath = path.join(session.path, 'code.sas')
|
||||||
|
|
||||||
// TODO: don't wait forever
|
// TODO: don't wait forever
|
||||||
while (
|
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
||||||
(await fileExists(codeFilePath)) &&
|
|
||||||
session.state !== SessionState.failed
|
|
||||||
) {}
|
|
||||||
|
|
||||||
if (session.state === SessionState.failed) {
|
if (session.crashed)
|
||||||
process.logger.error(
|
console.log('session crashed! while waiting to be ready', session.crashed)
|
||||||
'session crashed! while waiting to be ready',
|
|
||||||
session.failureReason
|
session.ready = true
|
||||||
)
|
|
||||||
} else {
|
|
||||||
session.state = SessionState.pending
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteSession(session: Session) {
|
public async deleteSession(session: Session) {
|
||||||
// remove the temporary files, to avoid buildup
|
// remove the temporary files, to avoid buildup
|
||||||
await deleteFolder(session.path)
|
await deleteFolder(session.path)
|
||||||
|
|
||||||
@@ -202,52 +138,24 @@ ${autoExecContent}`
|
|||||||
}
|
}
|
||||||
|
|
||||||
private scheduleSessionDestroy(session: Session) {
|
private scheduleSessionDestroy(session: Session) {
|
||||||
setTimeout(
|
setTimeout(async () => {
|
||||||
async () => {
|
if (session.inUse) {
|
||||||
if (session.state === SessionState.running) {
|
// adding 10 more minutes
|
||||||
// adding 10 more minutes
|
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||||
const newDeathTimeStamp =
|
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||||
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
|
|
||||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
|
||||||
|
|
||||||
this.scheduleSessionDestroy(session)
|
this.scheduleSessionDestroy(session)
|
||||||
} else {
|
} else {
|
||||||
const { expiresAfterMins } = session
|
await this.deleteSession(session)
|
||||||
|
}
|
||||||
// delay session destroy if expiresAfterMins present
|
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
|
||||||
if (expiresAfterMins && session.state !== SessionState.completed) {
|
|
||||||
// calculate session death time using expiresAfterMins
|
|
||||||
const newDeathTimeStamp =
|
|
||||||
parseInt(session.deathTimeStamp) +
|
|
||||||
expiresAfterMins.mins * 60 * 1000
|
|
||||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
|
||||||
|
|
||||||
// set expiresAfterMins to true to avoid using it again
|
|
||||||
session.expiresAfterMins!.used = true
|
|
||||||
|
|
||||||
this.scheduleSessionDestroy(session)
|
|
||||||
} else {
|
|
||||||
await this.deleteSession(session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSessionController = (
|
export const getSessionController = (): SessionController => {
|
||||||
runTime: RunTimeType
|
if (process.sessionController) return process.sessionController
|
||||||
): SessionController => {
|
|
||||||
if (runTime === RunTimeType.SAS) {
|
|
||||||
process.sasSessionController =
|
|
||||||
process.sasSessionController || new SASSessionController()
|
|
||||||
|
|
||||||
return process.sasSessionController
|
process.sessionController = new SessionController()
|
||||||
}
|
|
||||||
|
|
||||||
process.sessionController =
|
|
||||||
process.sessionController || new SessionController()
|
|
||||||
|
|
||||||
return process.sessionController
|
return process.sessionController
|
||||||
}
|
}
|
||||||
@@ -260,16 +168,9 @@ data _null_;
|
|||||||
rc=filename(fname,getoption('SYSIN') );
|
rc=filename(fname,getoption('SYSIN') );
|
||||||
if rc = 0 and fexist(fname) then rc=fdelete(fname);
|
if rc = 0 and fexist(fname) then rc=fdelete(fname);
|
||||||
rc=filename(fname);
|
rc=filename(fname);
|
||||||
/* now wait for the real SYSIN (location of code.sas) */
|
/* now wait for the real SYSIN */
|
||||||
slept=0;fname='';
|
slept=0;
|
||||||
do until (slept>(60*15));
|
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
|
||||||
rc=filename(fname,getoption('SYSIN'));
|
|
||||||
if rc = 0 and fexist(fname) then do;
|
|
||||||
putlog fname=;
|
|
||||||
rc=filename(fname);
|
|
||||||
rc=sleep(0.01,1); /* wait just a little more */
|
|
||||||
stop;
|
|
||||||
end;
|
|
||||||
slept=slept+sleep(0.01,1);
|
slept=slept+sleep(0.01,1);
|
||||||
end;
|
end;
|
||||||
stop;
|
stop;
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { escapeWinSlashes } from '@sasjs/utils'
|
|
||||||
import { PreProgramVars, Session } from '../../types'
|
|
||||||
import { generateFileUploadJSCode } from '../../utils'
|
|
||||||
import { ExecutionVars } from './'
|
|
||||||
|
|
||||||
export const createJSProgram = async (
|
|
||||||
program: string,
|
|
||||||
preProgramVariables: PreProgramVars,
|
|
||||||
vars: ExecutionVars,
|
|
||||||
session: Session,
|
|
||||||
weboutPath: string,
|
|
||||||
headersPath: string,
|
|
||||||
tokenFile: string,
|
|
||||||
otherArgs?: any
|
|
||||||
) => {
|
|
||||||
const varStatments = Object.keys(vars).reduce(
|
|
||||||
(computed: string, key: string) =>
|
|
||||||
`${computed}const ${key} = \`${vars[key]}\`;\n`,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
|
|
||||||
const preProgramVarStatments = `
|
|
||||||
let _webout = '';
|
|
||||||
const weboutPath = '${escapeWinSlashes(weboutPath)}';
|
|
||||||
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
|
||||||
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
|
||||||
const _SASJS_USERNAME = '${preProgramVariables?.username}';
|
|
||||||
const _SASJS_USERID = '${preProgramVariables?.userId}';
|
|
||||||
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
|
||||||
const _METAPERSON = _SASJS_DISPLAYNAME;
|
|
||||||
const _METAUSER = _SASJS_USERNAME;
|
|
||||||
const SASJSPROCESSMODE = 'Stored Program';
|
|
||||||
`
|
|
||||||
|
|
||||||
const requiredModules = `const fs = require('fs')`
|
|
||||||
|
|
||||||
program = `
|
|
||||||
/* runtime vars */
|
|
||||||
${varStatments}
|
|
||||||
|
|
||||||
/* dynamic user-provided vars */
|
|
||||||
${preProgramVarStatments}
|
|
||||||
|
|
||||||
/* actual job code */
|
|
||||||
${program}
|
|
||||||
|
|
||||||
/* write webout file only if webout exists*/
|
|
||||||
if (_webout) {
|
|
||||||
fs.writeFile(weboutPath, _webout, function (err) {
|
|
||||||
if (err) throw err;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
`
|
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
|
||||||
if (otherArgs?.filesNamesMap) {
|
|
||||||
const uploadJsCode = await generateFileUploadJSCode(
|
|
||||||
otherArgs.filesNamesMap,
|
|
||||||
session.path
|
|
||||||
)
|
|
||||||
|
|
||||||
// If any files are uploaded, the program needs to be updated with some
|
|
||||||
// dynamically generated variables (pointers) for ease of ingestion
|
|
||||||
if (uploadJsCode.length > 0) {
|
|
||||||
program = `${uploadJsCode}\n` + program
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return requiredModules + program
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { escapeWinSlashes } from '@sasjs/utils'
|
|
||||||
import { PreProgramVars, Session } from '../../types'
|
|
||||||
import { generateFileUploadPythonCode } from '../../utils'
|
|
||||||
import { ExecutionVars } from './'
|
|
||||||
|
|
||||||
export const createPythonProgram = async (
|
|
||||||
program: string,
|
|
||||||
preProgramVariables: PreProgramVars,
|
|
||||||
vars: ExecutionVars,
|
|
||||||
session: Session,
|
|
||||||
weboutPath: string,
|
|
||||||
headersPath: string,
|
|
||||||
tokenFile: string,
|
|
||||||
otherArgs?: any
|
|
||||||
) => {
|
|
||||||
const varStatments = Object.keys(vars).reduce(
|
|
||||||
(computed: string, key: string) => `${computed}${key} = '${vars[key]}';\n`,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
|
|
||||||
const preProgramVarStatments = `
|
|
||||||
_SASJS_SESSION_PATH = '${escapeWinSlashes(session.path)}';
|
|
||||||
_WEBOUT = '${escapeWinSlashes(weboutPath)}';
|
|
||||||
_SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
|
||||||
_SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
|
||||||
_SASJS_USERNAME = '${preProgramVariables?.username}';
|
|
||||||
_SASJS_USERID = '${preProgramVariables?.userId}';
|
|
||||||
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
|
||||||
_METAPERSON = _SASJS_DISPLAYNAME;
|
|
||||||
_METAUSER = _SASJS_USERNAME;
|
|
||||||
SASJSPROCESSMODE = 'Stored Program';
|
|
||||||
`
|
|
||||||
|
|
||||||
const requiredModules = `import os`
|
|
||||||
|
|
||||||
program = `
|
|
||||||
# runtime vars
|
|
||||||
${varStatments}
|
|
||||||
|
|
||||||
# dynamic user-provided vars
|
|
||||||
${preProgramVarStatments}
|
|
||||||
|
|
||||||
# change working directory to session folder
|
|
||||||
os.chdir(_SASJS_SESSION_PATH)
|
|
||||||
|
|
||||||
# actual job code
|
|
||||||
${program}
|
|
||||||
|
|
||||||
`
|
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
|
||||||
if (otherArgs?.filesNamesMap) {
|
|
||||||
const uploadPythonCode = await generateFileUploadPythonCode(
|
|
||||||
otherArgs.filesNamesMap,
|
|
||||||
session.path
|
|
||||||
)
|
|
||||||
|
|
||||||
// If any files are uploaded, the program needs to be updated with some
|
|
||||||
// dynamically generated variables (pointers) for ease of ingestion
|
|
||||||
if (uploadPythonCode.length > 0) {
|
|
||||||
program = `${uploadPythonCode}\n` + program
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return requiredModules + program
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { escapeWinSlashes } from '@sasjs/utils'
|
|
||||||
import { PreProgramVars, Session } from '../../types'
|
|
||||||
import { generateFileUploadRCode } from '../../utils'
|
|
||||||
import { ExecutionVars } from '.'
|
|
||||||
|
|
||||||
export const createRProgram = async (
|
|
||||||
program: string,
|
|
||||||
preProgramVariables: PreProgramVars,
|
|
||||||
vars: ExecutionVars,
|
|
||||||
session: Session,
|
|
||||||
weboutPath: string,
|
|
||||||
headersPath: string,
|
|
||||||
tokenFile: string,
|
|
||||||
otherArgs?: any
|
|
||||||
) => {
|
|
||||||
const varStatments = Object.keys(vars).reduce(
|
|
||||||
(computed: string, key: string) => `${computed}.${key} <- '${vars[key]}'\n`,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
|
|
||||||
const preProgramVarStatments = `
|
|
||||||
._SASJS_SESSION_PATH <- '${escapeWinSlashes(session.path)}';
|
|
||||||
._WEBOUT <- '${escapeWinSlashes(weboutPath)}';
|
|
||||||
._SASJS_WEBOUT_HEADERS <- '${escapeWinSlashes(headersPath)}';
|
|
||||||
._SASJS_TOKENFILE <- '${escapeWinSlashes(tokenFile)}';
|
|
||||||
._SASJS_USERNAME <- '${preProgramVariables?.username}';
|
|
||||||
._SASJS_USERID <- '${preProgramVariables?.userId}';
|
|
||||||
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
|
|
||||||
._METAPERSON <- ._SASJS_DISPLAYNAME;
|
|
||||||
._METAUSER <- ._SASJS_USERNAME;
|
|
||||||
SASJSPROCESSMODE <- 'Stored Program';
|
|
||||||
`
|
|
||||||
|
|
||||||
const requiredModules = ``
|
|
||||||
|
|
||||||
program = `
|
|
||||||
# runtime vars
|
|
||||||
${varStatments}
|
|
||||||
|
|
||||||
# dynamic user-provided vars
|
|
||||||
${preProgramVarStatments}
|
|
||||||
|
|
||||||
# change working directory to session folder
|
|
||||||
setwd(._SASJS_SESSION_PATH)
|
|
||||||
|
|
||||||
# actual job code
|
|
||||||
${program}
|
|
||||||
|
|
||||||
`
|
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
|
||||||
if (otherArgs?.filesNamesMap) {
|
|
||||||
const uploadRCode = await generateFileUploadRCode(
|
|
||||||
otherArgs.filesNamesMap,
|
|
||||||
session.path
|
|
||||||
)
|
|
||||||
|
|
||||||
// If any files are uploaded, the program needs to be updated with some
|
|
||||||
// dynamically generated variables (pointers) for ease of ingestion
|
|
||||||
if (uploadRCode.length > 0) {
|
|
||||||
program = `${uploadRCode}\n` + program
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return requiredModules + program
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { PreProgramVars, Session } from '../../types'
|
|
||||||
import { generateFileUploadSasCode, getMacrosFolder } from '../../utils'
|
|
||||||
import { ExecutionVars } from './'
|
|
||||||
|
|
||||||
export const createSASProgram = async (
|
|
||||||
program: string,
|
|
||||||
preProgramVariables: PreProgramVars,
|
|
||||||
vars: ExecutionVars,
|
|
||||||
session: Session,
|
|
||||||
weboutPath: string,
|
|
||||||
headersPath: string,
|
|
||||||
tokenFile: string,
|
|
||||||
otherArgs?: any
|
|
||||||
) => {
|
|
||||||
const varStatments = Object.keys(vars).reduce(
|
|
||||||
(computed: string, key: string) => `${computed}%let ${key}=${vars[key]};\n`,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
|
|
||||||
const preProgramVarStatments = `
|
|
||||||
%let _sasjs_tokenfile=${tokenFile};
|
|
||||||
%let _sasjs_username=${preProgramVariables?.username};
|
|
||||||
%let _sasjs_userid=${preProgramVariables?.userId};
|
|
||||||
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
|
||||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
|
||||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
|
||||||
%let _sasjs_webout_headers=${headersPath};
|
|
||||||
%let _metaperson=&_sasjs_displayname;
|
|
||||||
%let _metauser=&_sasjs_username;
|
|
||||||
|
|
||||||
/* the below is here for compatibility and will be removed in a future release */
|
|
||||||
%let sasjs_stpsrv_header_loc=&_sasjs_webout_headers;
|
|
||||||
|
|
||||||
%let sasjsprocessmode=Stored Program;
|
|
||||||
|
|
||||||
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
|
||||||
%macro _sasjs_server_init();
|
|
||||||
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
|
|
||||||
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
|
||||||
%mend;
|
|
||||||
%_sasjs_server_init()
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
program = `
|
|
||||||
options insert=(SASAUTOS="${getMacrosFolder()}");
|
|
||||||
|
|
||||||
/* runtime vars */
|
|
||||||
${varStatments}
|
|
||||||
filename _webout "${weboutPath}" mod;
|
|
||||||
|
|
||||||
/* dynamic user-provided vars */
|
|
||||||
${preProgramVarStatments}
|
|
||||||
|
|
||||||
/* user autoexec starts */
|
|
||||||
${otherArgs?.userAutoExec ?? ''}
|
|
||||||
/* user autoexec ends */
|
|
||||||
|
|
||||||
/* actual job code */
|
|
||||||
${program}`
|
|
||||||
|
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
|
||||||
if (otherArgs?.filesNamesMap) {
|
|
||||||
const uploadSasCode = await generateFileUploadSasCode(
|
|
||||||
otherArgs.filesNamesMap,
|
|
||||||
session.path
|
|
||||||
)
|
|
||||||
|
|
||||||
// If any files are uploaded, the program needs to be updated with some
|
|
||||||
// dynamically generated variables (pointers) for ease of ingestion
|
|
||||||
if (uploadSasCode.length > 0) {
|
|
||||||
program = `${uploadSasCode}` + program
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return program
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,3 @@ export * from './deploy'
|
|||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './Execution'
|
export * from './Execution'
|
||||||
export * from './FileUploadController'
|
export * from './FileUploadController'
|
||||||
export * from './createSASProgram'
|
|
||||||
export * from './createJSProgram'
|
|
||||||
export * from './createPythonProgram'
|
|
||||||
export * from './createRProgram'
|
|
||||||
export * from './processProgram'
|
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import { WriteStream, createWriteStream } from 'fs'
|
|
||||||
import { execFile } from 'child_process'
|
|
||||||
import { once } from 'stream'
|
|
||||||
import { createFile, moveFile } from '@sasjs/utils'
|
|
||||||
import { PreProgramVars, Session, SessionState } from '../../types'
|
|
||||||
import { RunTimeType } from '../../utils'
|
|
||||||
import {
|
|
||||||
ExecutionVars,
|
|
||||||
createSASProgram,
|
|
||||||
createJSProgram,
|
|
||||||
createPythonProgram,
|
|
||||||
createRProgram
|
|
||||||
} from './'
|
|
||||||
|
|
||||||
export const processProgram = async (
|
|
||||||
program: string,
|
|
||||||
preProgramVariables: PreProgramVars,
|
|
||||||
vars: ExecutionVars,
|
|
||||||
session: Session,
|
|
||||||
weboutPath: string,
|
|
||||||
headersPath: string,
|
|
||||||
tokenFile: string,
|
|
||||||
runTime: RunTimeType,
|
|
||||||
logPath: string,
|
|
||||||
otherArgs?: any
|
|
||||||
) => {
|
|
||||||
if (runTime === RunTimeType.SAS) {
|
|
||||||
program = await createSASProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
headersPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
|
|
||||||
const codePath = path.join(session.path, 'code.sas')
|
|
||||||
|
|
||||||
// Creating this file in a RUNNING session will break out
|
|
||||||
// the autoexec loop and actually execute the program
|
|
||||||
// but - given it will take several milliseconds to create
|
|
||||||
// (which can mean SAS trying to run a partial program, or
|
|
||||||
// failing due to file lock) we first create the file THEN
|
|
||||||
// we rename it.
|
|
||||||
await createFile(codePath + '.bkp', program)
|
|
||||||
await moveFile(codePath + '.bkp', codePath)
|
|
||||||
|
|
||||||
// we now need to poll the session status
|
|
||||||
while (session.state !== SessionState.completed) {
|
|
||||||
await delay(50)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let codePath: string
|
|
||||||
let executablePath: string
|
|
||||||
switch (runTime) {
|
|
||||||
case RunTimeType.JS:
|
|
||||||
program = await createJSProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
headersPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
codePath = path.join(session.path, 'code.js')
|
|
||||||
executablePath = process.nodeLoc!
|
|
||||||
|
|
||||||
break
|
|
||||||
case RunTimeType.PY:
|
|
||||||
program = await createPythonProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
headersPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
codePath = path.join(session.path, 'code.py')
|
|
||||||
executablePath = process.pythonLoc!
|
|
||||||
|
|
||||||
break
|
|
||||||
case RunTimeType.R:
|
|
||||||
program = await createRProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
headersPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
codePath = path.join(session.path, 'code.r')
|
|
||||||
executablePath = process.rLoc!
|
|
||||||
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid runtime!')
|
|
||||||
}
|
|
||||||
|
|
||||||
await createFile(codePath, program)
|
|
||||||
|
|
||||||
// create a stream that will write to console outputs to log file
|
|
||||||
const writeStream = createWriteStream(logPath)
|
|
||||||
// waiting for the open event so that we can have underlying file descriptor
|
|
||||||
await once(writeStream, 'open')
|
|
||||||
|
|
||||||
await execFilePromise(executablePath, [codePath], writeStream)
|
|
||||||
.then(() => {
|
|
||||||
session.state = SessionState.completed
|
|
||||||
|
|
||||||
process.logger.info('session completed', session)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
session.state = SessionState.failed
|
|
||||||
|
|
||||||
session.failureReason = err.toString()
|
|
||||||
|
|
||||||
process.logger.error(
|
|
||||||
'session crashed',
|
|
||||||
session.id,
|
|
||||||
session.failureReason
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// copy the code file to log and end write stream
|
|
||||||
writeStream.end(program)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Promisified child_process.execFile
|
|
||||||
*
|
|
||||||
* @param file - The name or path of the executable file to run.
|
|
||||||
* @param args - List of string arguments.
|
|
||||||
* @param writeStream - Child process stdout and stderr will be piped to it.
|
|
||||||
*
|
|
||||||
* @returns {Promise<{ stdout: string, stderr: string }>}
|
|
||||||
*/
|
|
||||||
const execFilePromise = (
|
|
||||||
file: string,
|
|
||||||
args: string[],
|
|
||||||
writeStream: WriteStream
|
|
||||||
): Promise<{ stdout: string; stderr: string }> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const child = execFile(file, args, (err, stdout, stderr) => {
|
|
||||||
if (err) reject(err)
|
|
||||||
|
|
||||||
resolve({ stdout, stderr })
|
|
||||||
})
|
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
|
||||||
writeStream.write(data)
|
|
||||||
})
|
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
|
||||||
writeStream.write(data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
import { readFile } from '@sasjs/utils'
|
|
||||||
import express from 'express'
|
|
||||||
import path from 'path'
|
|
||||||
import { Request, Post, Get } from 'tsoa'
|
|
||||||
import dotenv from 'dotenv'
|
|
||||||
import { ExecutionController } from './internal'
|
|
||||||
import {
|
|
||||||
getPreProgramVariables,
|
|
||||||
getRunTimeAndFilePath,
|
|
||||||
makeFilesNamesMap
|
|
||||||
} from '../utils'
|
|
||||||
import { MulterFile } from '../types/Upload'
|
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
export interface Sas9Response {
|
|
||||||
content: string
|
|
||||||
redirect?: string
|
|
||||||
error?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MockFileRead {
|
|
||||||
content: string
|
|
||||||
error?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MockSas9Controller {
|
|
||||||
private loggedIn: string | undefined
|
|
||||||
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
|
||||||
|
|
||||||
@Get('/SASStoredProcess')
|
|
||||||
public async sasStoredProcess(
|
|
||||||
@Request() req: express.Request
|
|
||||||
): Promise<Sas9Response> {
|
|
||||||
const username = req.query._username?.toString() || undefined
|
|
||||||
const password = req.query._password?.toString() || undefined
|
|
||||||
|
|
||||||
if (username && password) this.loggedIn = req.body.username
|
|
||||||
|
|
||||||
if (!this.loggedIn) {
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
redirect: '/SASLogon/login'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let program = req.query._program?.toString() || undefined
|
|
||||||
const filePath: string[] = program
|
|
||||||
? program.replace('/', '').split('/')
|
|
||||||
: ['generic', 'sas-stored-process']
|
|
||||||
|
|
||||||
if (program) {
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
this.mocksPath,
|
|
||||||
'sas9',
|
|
||||||
...filePath
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
'mocks',
|
|
||||||
'sas9',
|
|
||||||
...filePath
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/SASStoredProcess/do')
|
|
||||||
public async sasStoredProcessDoGet(
|
|
||||||
@Request() req: express.Request
|
|
||||||
): Promise<Sas9Response> {
|
|
||||||
const username = req.query._username?.toString() || undefined
|
|
||||||
const password = req.query._password?.toString() || undefined
|
|
||||||
|
|
||||||
if (username && password) this.loggedIn = username
|
|
||||||
|
|
||||||
if (!this.loggedIn) {
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
redirect: '/SASLogon/login'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const program = req.query._program ?? req.body?._program
|
|
||||||
const filePath: string[] = ['generic', 'sas-stored-process']
|
|
||||||
|
|
||||||
if (program) {
|
|
||||||
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
|
|
||||||
const otherArgs = {}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(
|
|
||||||
program + '.js'
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await new ExecutionController().executeFile({
|
|
||||||
programPath: codePath,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars: vars,
|
|
||||||
otherArgs: otherArgs,
|
|
||||||
runTime,
|
|
||||||
forceStringResult: true
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: result.result as string
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
process.logger.error('err', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: 'No webout returned.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
'mocks',
|
|
||||||
'sas9',
|
|
||||||
...filePath
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/SASStoredProcess/do/')
|
|
||||||
public async sasStoredProcessDoPost(
|
|
||||||
@Request() req: express.Request
|
|
||||||
): Promise<Sas9Response> {
|
|
||||||
if (!this.loggedIn) {
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
redirect: '/SASLogon/login'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isPublicAccount()) {
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
redirect: '/SASLogon/Login'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const program = req.query._program ?? req.body?._program
|
|
||||||
const vars = {
|
|
||||||
...req.query,
|
|
||||||
...req.body,
|
|
||||||
_requestMethod: req.method,
|
|
||||||
_driveLoc: process.driveLoc
|
|
||||||
}
|
|
||||||
const filesNamesMap = req.files?.length
|
|
||||||
? makeFilesNamesMap(req.files as MulterFile[])
|
|
||||||
: null
|
|
||||||
const otherArgs = { filesNamesMap: filesNamesMap }
|
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
|
|
||||||
try {
|
|
||||||
const result = await new ExecutionController().executeFile({
|
|
||||||
programPath: codePath,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars: vars,
|
|
||||||
otherArgs: otherArgs,
|
|
||||||
runTime,
|
|
||||||
session: req.sasjsSession,
|
|
||||||
forceStringResult: true
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: result.result as string
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
process.logger.error('err', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: 'No webout returned.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/SASLogon/login')
|
|
||||||
public async loginGet(): Promise<Sas9Response> {
|
|
||||||
if (this.loggedIn) {
|
|
||||||
if (this.isPublicAccount()) {
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
redirect: '/SASStoredProcess/Logoff?publicDenied=true'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
'mocks',
|
|
||||||
'sas9',
|
|
||||||
'generic',
|
|
||||||
'logged-in'
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
'mocks',
|
|
||||||
'sas9',
|
|
||||||
'generic',
|
|
||||||
'login'
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/SASLogon/login')
|
|
||||||
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
|
||||||
if (req.body.lt && req.body.lt !== 'validtoken')
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
redirect: '/SASLogon/login'
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loggedIn = req.body.username
|
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
'mocks',
|
|
||||||
'sas9',
|
|
||||||
'generic',
|
|
||||||
'logged-in'
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/SASLogon/logout')
|
|
||||||
public async logout(req: express.Request): Promise<Sas9Response> {
|
|
||||||
this.loggedIn = undefined
|
|
||||||
|
|
||||||
if (req.query.publicDenied === 'true') {
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
'mocks',
|
|
||||||
'sas9',
|
|
||||||
'generic',
|
|
||||||
'public-access-denied'
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
|
||||||
process.cwd(),
|
|
||||||
'mocks',
|
|
||||||
'sas9',
|
|
||||||
'generic',
|
|
||||||
'logged-out'
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/SASStoredProcess/Logoff') //publicDenied=true
|
|
||||||
public async logoff(req: express.Request): Promise<Sas9Response> {
|
|
||||||
const params = req.query.publicDenied
|
|
||||||
? `?publicDenied=${req.query.publicDenied}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
redirect: '/SASLogon/logout' + params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMockResponseFromFile = async (
|
|
||||||
filePath: string[]
|
|
||||||
): Promise<MockFileRead> => {
|
|
||||||
const filePathParsed = path.join(...filePath)
|
|
||||||
let error: boolean = false
|
|
||||||
|
|
||||||
let file = await readFile(filePathParsed).catch((err: any) => {
|
|
||||||
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
|
|
||||||
process.logger.error(errMsg)
|
|
||||||
|
|
||||||
error = true
|
|
||||||
|
|
||||||
return errMsg
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: file,
|
|
||||||
error: error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import {
|
|
||||||
Security,
|
|
||||||
Route,
|
|
||||||
Tags,
|
|
||||||
Path,
|
|
||||||
Example,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Patch,
|
|
||||||
Delete,
|
|
||||||
Body,
|
|
||||||
Request
|
|
||||||
} from 'tsoa'
|
|
||||||
|
|
||||||
import Permission from '../model/Permission'
|
|
||||||
import User from '../model/User'
|
|
||||||
import Group from '../model/Group'
|
|
||||||
import { UserResponse } from './user'
|
|
||||||
import { GroupDetailsResponse } from './group'
|
|
||||||
|
|
||||||
export enum PermissionType {
|
|
||||||
route = 'Route'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PrincipalType {
|
|
||||||
user = 'user',
|
|
||||||
group = 'group'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PermissionSettingForRoute {
|
|
||||||
grant = 'Grant',
|
|
||||||
deny = 'Deny'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegisterPermissionPayload {
|
|
||||||
/**
|
|
||||||
* Name of affected resource
|
|
||||||
* @example "/SASjsApi/code/execute"
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
/**
|
|
||||||
* Type of affected resource
|
|
||||||
* @example "Route"
|
|
||||||
*/
|
|
||||||
type: PermissionType
|
|
||||||
/**
|
|
||||||
* The indication of whether (and to what extent) access is provided
|
|
||||||
* @example "Grant"
|
|
||||||
*/
|
|
||||||
setting: PermissionSettingForRoute
|
|
||||||
/**
|
|
||||||
* Indicates the type of principal
|
|
||||||
* @example "user"
|
|
||||||
*/
|
|
||||||
principalType: PrincipalType
|
|
||||||
/**
|
|
||||||
* The id of user or group to which a rule is assigned.
|
|
||||||
* @example 123
|
|
||||||
*/
|
|
||||||
principalId: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdatePermissionPayload {
|
|
||||||
/**
|
|
||||||
* The indication of whether (and to what extent) access is provided
|
|
||||||
* @example "Grant"
|
|
||||||
*/
|
|
||||||
setting: PermissionSettingForRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PermissionDetailsResponse {
|
|
||||||
permissionId: number
|
|
||||||
path: string
|
|
||||||
type: string
|
|
||||||
setting: string
|
|
||||||
user?: UserResponse
|
|
||||||
group?: GroupDetailsResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
|
||||||
@Route('SASjsApi/permission')
|
|
||||||
@Tags('Permission')
|
|
||||||
export class PermissionController {
|
|
||||||
/**
|
|
||||||
* Get the list of permission rules applicable the authenticated user.
|
|
||||||
* If the user is an admin, all rules are returned.
|
|
||||||
*
|
|
||||||
* @summary Get the list of permission rules. If the user is admin, all rules are returned.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<PermissionDetailsResponse[]>([
|
|
||||||
{
|
|
||||||
permissionId: 123,
|
|
||||||
path: '/SASjsApi/code/execute',
|
|
||||||
type: 'Route',
|
|
||||||
setting: 'Grant',
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
username: 'johnSnow01',
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permissionId: 124,
|
|
||||||
path: '/SASjsApi/code/execute',
|
|
||||||
type: 'Route',
|
|
||||||
setting: 'Grant',
|
|
||||||
group: {
|
|
||||||
groupId: 1,
|
|
||||||
name: 'DCGroup',
|
|
||||||
description: 'This group represents Data Controller Users',
|
|
||||||
isActive: true,
|
|
||||||
users: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
@Get('/')
|
|
||||||
public async getAllPermissions(
|
|
||||||
@Request() request: express.Request
|
|
||||||
): Promise<PermissionDetailsResponse[]> {
|
|
||||||
return getAllPermissions(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Create a new permission. Admin only.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<PermissionDetailsResponse>({
|
|
||||||
permissionId: 123,
|
|
||||||
path: '/SASjsApi/code/execute',
|
|
||||||
type: 'Route',
|
|
||||||
setting: 'Grant',
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
username: 'johnSnow01',
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Post('/')
|
|
||||||
public async createPermission(
|
|
||||||
@Body() body: RegisterPermissionPayload
|
|
||||||
): Promise<PermissionDetailsResponse> {
|
|
||||||
return createPermission(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Update permission setting. Admin only
|
|
||||||
* @param permissionId The permission's identifier
|
|
||||||
* @example permissionId 1234
|
|
||||||
*/
|
|
||||||
@Example<PermissionDetailsResponse>({
|
|
||||||
permissionId: 123,
|
|
||||||
path: '/SASjsApi/code/execute',
|
|
||||||
type: 'Route',
|
|
||||||
setting: 'Grant',
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
username: 'johnSnow01',
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Patch('{permissionId}')
|
|
||||||
public async updatePermission(
|
|
||||||
@Path() permissionId: number,
|
|
||||||
@Body() body: UpdatePermissionPayload
|
|
||||||
): Promise<PermissionDetailsResponse> {
|
|
||||||
return updatePermission(permissionId, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Delete a permission. Admin only.
|
|
||||||
* @param permissionId The user's identifier
|
|
||||||
* @example permissionId 1234
|
|
||||||
*/
|
|
||||||
@Delete('{permissionId}')
|
|
||||||
public async deletePermission(@Path() permissionId: number) {
|
|
||||||
return deletePermission(permissionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllPermissions = async (
|
|
||||||
req: express.Request
|
|
||||||
): Promise<PermissionDetailsResponse[]> => {
|
|
||||||
const { user } = req
|
|
||||||
|
|
||||||
if (user?.isAdmin) return await Permission.get({})
|
|
||||||
else {
|
|
||||||
const permissions: PermissionDetailsResponse[] = []
|
|
||||||
|
|
||||||
const dbUser = await User.findOne({ id: user?.userId })
|
|
||||||
if (!dbUser)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'User not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
permissions.push(...(await Permission.get({ user: dbUser._id })))
|
|
||||||
|
|
||||||
for (const group of dbUser.groups) {
|
|
||||||
permissions.push(...(await Permission.get({ group })))
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createPermission = async ({
|
|
||||||
path,
|
|
||||||
type,
|
|
||||||
setting,
|
|
||||||
principalType,
|
|
||||||
principalId
|
|
||||||
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
|
||||||
const permission = new Permission({
|
|
||||||
path,
|
|
||||||
type,
|
|
||||||
setting
|
|
||||||
})
|
|
||||||
|
|
||||||
let user: UserResponse | undefined
|
|
||||||
let group: GroupDetailsResponse | undefined
|
|
||||||
|
|
||||||
switch (principalType) {
|
|
||||||
case PrincipalType.user: {
|
|
||||||
const userInDB = await User.findOne({ id: principalId })
|
|
||||||
if (!userInDB)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'User not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userInDB.isAdmin)
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: 'Can not add permission for admin user.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
|
||||||
path,
|
|
||||||
type,
|
|
||||||
user: userInDB._id
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alreadyExists)
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message:
|
|
||||||
'Permission already exists with provided Path, Type and User.'
|
|
||||||
}
|
|
||||||
|
|
||||||
permission.user = userInDB._id
|
|
||||||
|
|
||||||
user = {
|
|
||||||
id: userInDB.id,
|
|
||||||
username: userInDB.username,
|
|
||||||
displayName: userInDB.displayName,
|
|
||||||
isAdmin: userInDB.isAdmin
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case PrincipalType.group: {
|
|
||||||
const groupInDB = await Group.findOne({ groupId: principalId })
|
|
||||||
if (!groupInDB)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Group not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
|
||||||
path,
|
|
||||||
type,
|
|
||||||
group: groupInDB._id
|
|
||||||
})
|
|
||||||
if (alreadyExists)
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message:
|
|
||||||
'Permission already exists with provided Path, Type and Group.'
|
|
||||||
}
|
|
||||||
|
|
||||||
permission.group = groupInDB._id
|
|
||||||
|
|
||||||
group = {
|
|
||||||
groupId: groupInDB.groupId,
|
|
||||||
name: groupInDB.name,
|
|
||||||
description: groupInDB.description,
|
|
||||||
isActive: groupInDB.isActive,
|
|
||||||
users: groupInDB.populate({
|
|
||||||
path: 'users',
|
|
||||||
select: 'id username displayName isAdmin -_id',
|
|
||||||
options: { limit: 15 }
|
|
||||||
}) as unknown as UserResponse[]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: 'Invalid principal type. Valid types are user or group.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedPermission = await permission.save()
|
|
||||||
|
|
||||||
return {
|
|
||||||
permissionId: savedPermission.permissionId,
|
|
||||||
path: savedPermission.path,
|
|
||||||
type: savedPermission.type,
|
|
||||||
setting: savedPermission.setting,
|
|
||||||
user,
|
|
||||||
group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePermission = async (
|
|
||||||
id: number,
|
|
||||||
data: UpdatePermissionPayload
|
|
||||||
): Promise<PermissionDetailsResponse> => {
|
|
||||||
const { setting } = data
|
|
||||||
|
|
||||||
const updatedPermission = (await Permission.findOneAndUpdate(
|
|
||||||
{ permissionId: id },
|
|
||||||
{ setting },
|
|
||||||
{ new: true }
|
|
||||||
)
|
|
||||||
.select({
|
|
||||||
_id: 0,
|
|
||||||
permissionId: 1,
|
|
||||||
path: 1,
|
|
||||||
type: 1,
|
|
||||||
setting: 1
|
|
||||||
})
|
|
||||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
|
||||||
.populate({
|
|
||||||
path: 'group',
|
|
||||||
select: 'groupId name description -_id'
|
|
||||||
})) as unknown as PermissionDetailsResponse
|
|
||||||
if (!updatedPermission)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Permission not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletePermission = async (id: number) => {
|
|
||||||
const permission = await Permission.findOne({ permissionId: id })
|
|
||||||
if (!permission)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Permission not found.'
|
|
||||||
}
|
|
||||||
await Permission.deleteOne({ permissionId: id })
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
import { getSessionController } from './internal'
|
|
||||||
import { SessionState } from '../types'
|
|
||||||
|
|
||||||
interface SessionResponse extends UserResponse {
|
|
||||||
needsToUpdatePassword: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/session')
|
@Route('SASjsApi/session')
|
||||||
@@ -19,53 +13,18 @@ export class SessionController {
|
|||||||
@Example<UserResponse>({
|
@Example<UserResponse>({
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John',
|
displayName: 'John'
|
||||||
isAdmin: false
|
|
||||||
})
|
})
|
||||||
@Get('/')
|
@Get('/')
|
||||||
public async session(
|
public async session(
|
||||||
@Request() request: express.Request
|
@Request() request: express.Request
|
||||||
): Promise<SessionResponse> {
|
): Promise<UserResponse> {
|
||||||
return session(request)
|
return session(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The polling endpoint is currently implemented for single-server deployments only.<br>
|
|
||||||
* Load balanced / grid topologies will be supported in a future release.<br>
|
|
||||||
* If your site requires this, please reach out to SASjs Support.
|
|
||||||
* @summary Get session state (initialising, pending, running, completed, failed).
|
|
||||||
* @example completed
|
|
||||||
*/
|
|
||||||
@Get('/:sessionId/state')
|
|
||||||
public async sessionState(sessionId: string): Promise<SessionState> {
|
|
||||||
return sessionState(sessionId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
isAdmin: req.user!.isAdmin,
|
|
||||||
needsToUpdatePassword: req.user!.needsToUpdatePassword
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const sessionState = (sessionId: string): SessionState => {
|
|
||||||
for (let runTime of process.runTimes) {
|
|
||||||
// get session controller for each available runTime
|
|
||||||
const sessionController = getSessionController(runTime)
|
|
||||||
|
|
||||||
// get session by sessionId
|
|
||||||
const session = sessionController.getSessionById(sessionId)
|
|
||||||
|
|
||||||
// return session state if session was found
|
|
||||||
if (session) {
|
|
||||||
return session.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
message: `Session with ID '${sessionId}' was not found.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
import path from 'path'
|
||||||
import {
|
import {
|
||||||
|
Request,
|
||||||
|
Security,
|
||||||
|
Route,
|
||||||
|
Tags,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
Example
|
||||||
|
} from 'tsoa'
|
||||||
|
import {
|
||||||
|
ExecuteReturnJson,
|
||||||
|
ExecuteReturnRaw,
|
||||||
ExecutionController,
|
ExecutionController,
|
||||||
ExecutionVars,
|
ExecutionVars
|
||||||
getSessionController
|
|
||||||
} from './internal'
|
} from './internal'
|
||||||
|
import { PreProgramVars } from '../types'
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getTmpFilesFolderPath,
|
||||||
|
HTTPHeaders,
|
||||||
|
isDebugOn,
|
||||||
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
getRunTimeAndFilePath
|
parseLogToArray
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { MulterFile } from '../types/Upload'
|
|
||||||
|
|
||||||
interface ExecutePostRequestPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
* Location of SAS program
|
* Location of SAS program
|
||||||
* @example "/Public/somefolder/some.file"
|
* @example "/Public/somefolder/some.file"
|
||||||
@@ -20,34 +35,15 @@ interface ExecutePostRequestPayload {
|
|||||||
_program?: string
|
_program?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TriggerProgramPayload {
|
interface IRecordOfAny {
|
||||||
/**
|
[key: string]: any
|
||||||
* Location of SAS program.
|
|
||||||
* @example "/Public/somefolder/some.file"
|
|
||||||
*/
|
|
||||||
_program: string
|
|
||||||
/**
|
|
||||||
* Amount of minutes after the completion of the program when the session must be
|
|
||||||
* destroyed.
|
|
||||||
* @example 15
|
|
||||||
*/
|
|
||||||
expiresAfterMins?: number
|
|
||||||
/**
|
|
||||||
* Query param for setting debug mode.
|
|
||||||
*/
|
|
||||||
_debug?: number
|
|
||||||
}
|
}
|
||||||
|
export interface ExecuteReturnJsonResponse {
|
||||||
interface TriggerProgramResponse {
|
status: string
|
||||||
/**
|
_webout: string | IRecordOfAny
|
||||||
* `sessionId` is the ID of the session and the name of the temporary folder
|
log: LogLine[]
|
||||||
* used to store program outputs.<br><br>
|
message?: string
|
||||||
* For SAS, this would be the location of the SASWORK folder.<br><br>
|
httpHeaders: HTTPHeaders
|
||||||
* `sessionId` can be used to poll session state using the
|
|
||||||
* GET /SASjsApi/session/{sessionId}/state endpoint.
|
|
||||||
* @example "20241028074744-54132-1730101664824"
|
|
||||||
*/
|
|
||||||
sessionId: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@@ -55,106 +51,105 @@ interface TriggerProgramResponse {
|
|||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
export class STPController {
|
export class STPController {
|
||||||
/**
|
/**
|
||||||
* Trigger a Stored Program using the _program URL parameter.
|
* Trigger a SAS program using it's location in the _program URL parameter.
|
||||||
|
* Enable debugging using the _debug URL parameter. Setting _debug=131 will
|
||||||
|
* cause the log to be streamed in the output.
|
||||||
*
|
*
|
||||||
* Accepts additional URL parameters (converted to session variables)
|
* Additional URL parameters are turned into SAS macro variables.
|
||||||
* and file uploads. For more details, see docs:
|
|
||||||
*
|
*
|
||||||
* https://server.sasjs.io/storedprograms
|
* Any files provided in the request body are placed into the SAS session with
|
||||||
|
* corresponding _WEBIN_XXX variables created.
|
||||||
*
|
*
|
||||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
* The response headers can be adjusted using the mfs_httpheader() macro. Any
|
||||||
* @param _program Location of Stored Program in SASjs Drive.
|
* file type can be returned, including binary files such as zip or xls.
|
||||||
* @param _debug Optional query param for setting debug mode (returns the session log in the response body).
|
*
|
||||||
* @example _program "/Projects/myApp/some/program"
|
* If _debug is >= 131, response headers will contain Content-Type: 'text/plain'
|
||||||
* @example _debug 131
|
*
|
||||||
|
* This behaviour differs for POST requests, in which case the response is
|
||||||
|
* always JSON.
|
||||||
|
*
|
||||||
|
* @summary Execute Stored Program, return raw _webout content.
|
||||||
|
* @param _program Location of SAS program
|
||||||
|
* @example _program "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Get('/execute')
|
@Get('/execute')
|
||||||
public async executeGetRequest(
|
public async executeReturnRaw(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Query() _program: string,
|
@Query() _program: string
|
||||||
@Query() _debug?: number
|
|
||||||
): Promise<string | Buffer> {
|
): Promise<string | Buffer> {
|
||||||
let vars = request.query as ExecutionVars
|
return executeReturnRaw(request, _program)
|
||||||
if (_debug) {
|
}
|
||||||
vars = {
|
|
||||||
...vars,
|
/**
|
||||||
_debug
|
* Trigger a SAS program using it's location in the _program URL parameter.
|
||||||
}
|
* Enable debugging using the _debug URL parameter. In any case, the log is
|
||||||
|
* always returned in the log object.
|
||||||
|
*
|
||||||
|
* Additional URL parameters are turned into SAS macro variables.
|
||||||
|
*
|
||||||
|
* Any files provided in the request body are placed into the SAS session with
|
||||||
|
* corresponding _WEBIN_XXX variables created.
|
||||||
|
*
|
||||||
|
* The response will be a JSON object with the following root attributes: log,
|
||||||
|
* webout, headers.
|
||||||
|
*
|
||||||
|
* The webout will be a nested JSON object ONLY if the response-header
|
||||||
|
* contains a content-type of application/json AND it is valid JSON.
|
||||||
|
* Otherwise it will be a stringified version of the webout content.
|
||||||
|
*
|
||||||
|
* Response headers from the mfs_httpheader macro are simply listed in the
|
||||||
|
* headers object, for POST requests they have no effect on the actual
|
||||||
|
* response header.
|
||||||
|
*
|
||||||
|
* @summary Execute Stored Program, return JSON
|
||||||
|
* @param _program Location of SAS program
|
||||||
|
* @example _program "/Public/somefolder/some.file"
|
||||||
|
*/
|
||||||
|
@Example<ExecuteReturnJsonResponse>({
|
||||||
|
status: 'success',
|
||||||
|
_webout: 'webout content',
|
||||||
|
log: [],
|
||||||
|
httpHeaders: {
|
||||||
|
'Content-type': 'application/zip',
|
||||||
|
'Cache-Control': 'public, max-age=1000'
|
||||||
}
|
}
|
||||||
|
})
|
||||||
return execute(request, _program, vars)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger a Stored Program using the _program URL parameter.
|
|
||||||
*
|
|
||||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
|
||||||
*
|
|
||||||
* https://server.sasjs.io/storedprograms
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
|
||||||
* @param _program Location of code in SASjs Drive
|
|
||||||
* @example _program "/Projects/myApp/some/program"
|
|
||||||
*/
|
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executePostRequest(
|
public async executeReturnJson(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body?: ExecutePostRequestPayload,
|
@Body() body?: ExecuteReturnJsonPayload,
|
||||||
@Query() _program?: string
|
@Query() _program?: string
|
||||||
): Promise<string | Buffer> {
|
): Promise<ExecuteReturnJsonResponse> {
|
||||||
const program = _program ?? body?._program
|
const program = _program ?? body?._program
|
||||||
const vars = { ...request.query, ...request.body }
|
return executeReturnJson(request, program!)
|
||||||
const filesNamesMap = request.files?.length
|
|
||||||
? makeFilesNamesMap(request.files as MulterFile[])
|
|
||||||
: null
|
|
||||||
const otherArgs = { filesNamesMap: filesNamesMap }
|
|
||||||
|
|
||||||
return execute(request, program!, vars, otherArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger Program on the Specified Runtime.
|
|
||||||
* @summary Triggers program and returns SessionId immediately - does not wait for program completion.
|
|
||||||
* @param _program Location of code in SASjs Drive.
|
|
||||||
* @param expiresAfterMins Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.
|
|
||||||
* @param _debug Optional query param for setting debug mode.
|
|
||||||
* @example _program "/Projects/myApp/some/program"
|
|
||||||
* @example _debug 131
|
|
||||||
* @example expiresAfterMins 15
|
|
||||||
*/
|
|
||||||
@Post('/trigger')
|
|
||||||
public async triggerProgram(
|
|
||||||
@Request() request: express.Request,
|
|
||||||
@Query() _program: string,
|
|
||||||
@Query() _debug?: number,
|
|
||||||
@Query() expiresAfterMins?: number
|
|
||||||
): Promise<TriggerProgramResponse> {
|
|
||||||
return triggerProgram(request, { _program, _debug, expiresAfterMins })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const execute = async (
|
const executeReturnRaw = async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string,
|
_program: string
|
||||||
vars: ExecutionVars,
|
|
||||||
otherArgs?: any
|
|
||||||
): Promise<string | Buffer> => {
|
): Promise<string | Buffer> => {
|
||||||
|
const query = req.query as ExecutionVars
|
||||||
|
const sasCodePath =
|
||||||
|
path
|
||||||
|
.join(getTmpFilesFolderPath(), _program)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
const { result, httpHeaders } =
|
||||||
|
(await new ExecutionController().executeFile(
|
||||||
|
sasCodePath,
|
||||||
|
getPreProgramVariables(req),
|
||||||
|
query
|
||||||
|
)) as ExecuteReturnRaw
|
||||||
|
|
||||||
const { result, httpHeaders } = await new ExecutionController().executeFile(
|
// Should over-ride response header for debug
|
||||||
{
|
// on GET request to see entire log rendering on browser.
|
||||||
programPath: codePath,
|
if (isDebugOn(query)) {
|
||||||
runTime,
|
httpHeaders['content-type'] = 'text/plain'
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
}
|
||||||
vars,
|
|
||||||
otherArgs,
|
|
||||||
session: req.sasjsSession
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
req.res?.header(httpHeaders)
|
req.res?.set(httpHeaders)
|
||||||
|
|
||||||
if (result instanceof Buffer) {
|
if (result instanceof Buffer) {
|
||||||
;(req as any).sasHeaders = httpHeaders
|
;(req as any).sasHeaders = httpHeaders
|
||||||
@@ -171,45 +166,41 @@ const execute = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerProgram = async (
|
const executeReturnJson = async (
|
||||||
req: express.Request,
|
req: any,
|
||||||
{ _program, _debug, expiresAfterMins }: TriggerProgramPayload
|
_program: string
|
||||||
): Promise<TriggerProgramResponse> => {
|
): Promise<ExecuteReturnJsonResponse> => {
|
||||||
|
const sasCodePath =
|
||||||
|
path
|
||||||
|
.join(getTmpFilesFolderPath(), _program)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
|
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// put _program query param into vars object
|
const { webout, log, httpHeaders } =
|
||||||
const vars: { [key: string]: string | number } = { _program }
|
(await new ExecutionController().executeFile(
|
||||||
|
sasCodePath,
|
||||||
|
getPreProgramVariables(req),
|
||||||
|
{ ...req.query, ...req.body },
|
||||||
|
{ filesNamesMap: filesNamesMap },
|
||||||
|
true,
|
||||||
|
req.sasSession
|
||||||
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
// if present add _debug query param to vars object
|
let weboutRes: string | IRecordOfAny = webout
|
||||||
if (_debug) {
|
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
||||||
vars._debug = _debug
|
try {
|
||||||
|
weboutRes = JSON.parse(webout as string)
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get code path and runTime
|
return {
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
status: 'success',
|
||||||
|
_webout: weboutRes,
|
||||||
// get session controller based on runTime
|
log: parseLogToArray(log),
|
||||||
const sessionController = getSessionController(runTime)
|
httpHeaders
|
||||||
|
|
||||||
// get session
|
|
||||||
const session = await sessionController.getSession()
|
|
||||||
|
|
||||||
// add expiresAfterMins to session if provided
|
|
||||||
if (expiresAfterMins) {
|
|
||||||
// expiresAfterMins.used is set initially to false
|
|
||||||
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// call executeFile method of ExecutionController without awaiting
|
|
||||||
new ExecutionController().executeFile({
|
|
||||||
programPath: codePath,
|
|
||||||
runTime,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars,
|
|
||||||
session
|
|
||||||
})
|
|
||||||
|
|
||||||
// return session id
|
|
||||||
return { sessionId: session.id }
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
@@ -219,3 +210,16 @@ const triggerProgram = 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,35 +10,23 @@ 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,
|
|
||||||
ALL_USERS_GROUP
|
|
||||||
} from '../utils'
|
|
||||||
import { GroupController, GroupResponse } from './group'
|
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserDetailsResponse {
|
interface UserDetailsResponse {
|
||||||
id: number
|
id: number
|
||||||
displayName: string
|
displayName: string
|
||||||
username: string
|
username: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
autoExec?: string
|
|
||||||
groups?: GroupResponse[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@@ -54,14 +41,12 @@ export class UserController {
|
|||||||
{
|
{
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John',
|
displayName: 'John'
|
||||||
isAdmin: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 456,
|
id: 456,
|
||||||
username: 'starkusername',
|
username: 'starkusername',
|
||||||
displayName: 'Stark',
|
displayName: 'Stark'
|
||||||
isAdmin: true
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@Get('/')
|
@Get('/')
|
||||||
@@ -88,68 +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.
|
|
||||||
* @param username The User's username
|
|
||||||
* @example username "johnSnow01"
|
|
||||||
*/
|
|
||||||
@Get('by/username/{username}')
|
|
||||||
public async getUserByUsername(
|
|
||||||
@Request() req: express.Request,
|
|
||||||
@Path() username: string
|
|
||||||
): Promise<UserDetailsResponse> {
|
|
||||||
const { MODE } = process.env
|
|
||||||
|
|
||||||
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
|
|
||||||
|
|
||||||
const { user } = req
|
|
||||||
const getAutoExec = user!.isAdmin || user!.username == username
|
|
||||||
return getUser({ username }, getAutoExec)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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({ id: userId }, getAutoExec)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Update user properties - such as displayName. Can be performed either by admins, or the user in question.
|
|
||||||
* @param username The User's username
|
|
||||||
* @example username "johnSnow01"
|
|
||||||
*/
|
|
||||||
@Example<UserDetailsResponse>({
|
|
||||||
id: 1234,
|
|
||||||
displayName: 'John Snow',
|
|
||||||
username: 'johnSnow01',
|
|
||||||
isAdmin: false,
|
|
||||||
isActive: true
|
|
||||||
})
|
|
||||||
@Patch('by/username/{username}')
|
|
||||||
public async updateUserByUsername(
|
|
||||||
@Path() username: string,
|
|
||||||
@Body() body: UserPayload
|
|
||||||
): Promise<UserDetailsResponse> {
|
|
||||||
const { MODE } = process.env
|
|
||||||
|
|
||||||
if (MODE === ModeType.Desktop)
|
|
||||||
return updateDesktopAutoExec(body.autoExec ?? '')
|
|
||||||
|
|
||||||
return updateUser({ username }, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,26 +99,7 @@ export class UserController {
|
|||||||
@Path() userId: number,
|
@Path() userId: number,
|
||||||
@Body() body: UserPayload
|
@Body() body: UserPayload
|
||||||
): Promise<UserDetailsResponse> {
|
): Promise<UserDetailsResponse> {
|
||||||
const { MODE } = process.env
|
return updateUser(userId, body)
|
||||||
|
|
||||||
if (MODE === ModeType.Desktop)
|
|
||||||
return updateDesktopAutoExec(body.autoExec ?? '')
|
|
||||||
|
|
||||||
return updateUser({ id: userId }, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Delete a user. Can be performed either by admins, or the user in question.
|
|
||||||
* @param username The User's username
|
|
||||||
* @example username "johnSnow01"
|
|
||||||
*/
|
|
||||||
@Delete('by/username/{username}')
|
|
||||||
public async deleteUserByUsername(
|
|
||||||
@Path() username: string,
|
|
||||||
@Body() body: { password?: string },
|
|
||||||
@Query() @Hidden() isAdmin: boolean = false
|
|
||||||
) {
|
|
||||||
return deleteUser({ username }, isAdmin, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,25 +113,21 @@ export class UserController {
|
|||||||
@Body() body: { password?: string },
|
@Body() body: { password?: string },
|
||||||
@Query() @Hidden() isAdmin: boolean = false
|
@Query() @Hidden() isAdmin: boolean = false
|
||||||
) {
|
) {
|
||||||
return deleteUser({ id: userId }, isAdmin, body)
|
return deleteUser(userId, isAdmin, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllUsers = async (): Promise<UserResponse[]> =>
|
const getAllUsers = async (): Promise<UserResponse[]> =>
|
||||||
await User.find({})
|
await User.find({})
|
||||||
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
|
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
|
||||||
.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 })
|
||||||
if (usernameExist)
|
if (usernameExist) throw new Error('Username already exists.')
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
message: 'Username already exists.'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash passwords
|
// Hash passwords
|
||||||
const hashPassword = User.hashPassword(password)
|
const hashPassword = User.hashPassword(password)
|
||||||
@@ -231,112 +138,48 @@ 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()
|
||||||
|
|
||||||
const groupController = new GroupController()
|
|
||||||
const allUsersGroup = await groupController
|
|
||||||
.getGroupByGroupName(ALL_USERS_GROUP.name)
|
|
||||||
.catch(() => {})
|
|
||||||
|
|
||||||
if (allUsersGroup) {
|
|
||||||
await groupController.addUserToGroup(allUsersGroup.groupId, savedUser.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: savedUser.id,
|
id: savedUser.id,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetUserBy {
|
const getUser = async (id: number): Promise<UserDetailsResponse> => {
|
||||||
id?: number
|
const user = await User.findOne({ id })
|
||||||
username?: string
|
.select({
|
||||||
}
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
username: 1,
|
||||||
|
displayName: 1,
|
||||||
|
isAdmin: 1,
|
||||||
|
isActive: 1
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
|
if (!user) throw new Error('User is not found.')
|
||||||
|
|
||||||
const getUser = async (
|
return user
|
||||||
findBy: GetUserBy,
|
|
||||||
getAutoExec: boolean
|
|
||||||
): Promise<UserDetailsResponse> => {
|
|
||||||
const user = (await User.findOne(
|
|
||||||
findBy,
|
|
||||||
`id displayName username isActive isAdmin autoExec -_id`
|
|
||||||
).populate(
|
|
||||||
'groups',
|
|
||||||
'groupId name description -_id'
|
|
||||||
)) as unknown as UserDetailsResponse
|
|
||||||
|
|
||||||
if (!user)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
message: 'User is not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
displayName: user.displayName,
|
|
||||||
username: user.username,
|
|
||||||
isActive: user.isActive,
|
|
||||||
isAdmin: user.isAdmin,
|
|
||||||
autoExec: getAutoExec ? (user.autoExec ?? '') : undefined,
|
|
||||||
groups: user.groups
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDesktopAutoExec = async () => {
|
|
||||||
return {
|
|
||||||
...desktopUser,
|
|
||||||
id: desktopUser.userId,
|
|
||||||
autoExec: await getUserAutoExec()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUser = async (
|
const updateUser = async (
|
||||||
findBy: GetUserBy,
|
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 }
|
||||||
|
|
||||||
const user = await User.findOne(findBy)
|
|
||||||
|
|
||||||
if (username && username !== user?.username && user?.authProvider) {
|
|
||||||
throw {
|
|
||||||
code: 405,
|
|
||||||
message:
|
|
||||||
'Can not update username of user that is created by an external auth provider.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayName && displayName !== user?.displayName && user?.authProvider) {
|
|
||||||
throw {
|
|
||||||
code: 405,
|
|
||||||
message:
|
|
||||||
'Can not update display name of user that is created by an external auth provider.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
if (usernameExist?.id != id) throw new Error('Username already exists.')
|
||||||
if (
|
|
||||||
(findBy.id && usernameExist.id != findBy.id) ||
|
|
||||||
(findBy.username && usernameExist.username != findBy.username)
|
|
||||||
)
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
message: 'Username already exists.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
params.username = username
|
params.username = username
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,53 +188,33 @@ const updateUser = async (
|
|||||||
params.password = User.hashPassword(password)
|
params.password = User.hashPassword(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await User.findOneAndUpdate(findBy, 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)
|
return updatedUser
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
message: `Unable to find user with ${findBy.id || findBy.username}`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
||||||
findBy: GetUserBy,
|
id: number,
|
||||||
isAdmin: boolean,
|
isAdmin: boolean,
|
||||||
{ password }: { password?: string }
|
{ password }: { password?: string }
|
||||||
) => {
|
) => {
|
||||||
const user = await User.findOne(findBy)
|
const user = await User.findOne({ id })
|
||||||
if (!user)
|
if (!user) throw new Error('User is not found.')
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
message: 'User is not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
const validPass = user.comparePassword(password!)
|
const validPass = user.comparePassword(password!)
|
||||||
if (!validPass)
|
if (!validPass) throw new Error('Invalid password.')
|
||||||
throw {
|
|
||||||
code: 401,
|
|
||||||
message: 'Invalid password.'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.deleteOne(findBy)
|
await User.deleteOne({ id })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import express from 'express'
|
|
||||||
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
|
|
||||||
import { readFile, convertSecondsToHms } from '@sasjs/utils'
|
|
||||||
|
|
||||||
import User from '../model/User'
|
|
||||||
import Client from '../model/Client'
|
|
||||||
import {
|
|
||||||
getWebBuildFolder,
|
|
||||||
generateAuthCode,
|
|
||||||
RateLimiter,
|
|
||||||
AuthProviderType,
|
|
||||||
LDAPClient
|
|
||||||
} 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 Destroy the session stored in cookies
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Get('/SASLogon/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 })
|
|
||||||
|
|
||||||
let validPass = false
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
if (
|
|
||||||
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
|
||||||
user.authProvider === AuthProviderType.LDAP
|
|
||||||
) {
|
|
||||||
const ldapClient = await LDAPClient.init()
|
|
||||||
validPass = await ldapClient
|
|
||||||
.verifyUser(username, password)
|
|
||||||
.catch(() => false)
|
|
||||||
} else {
|
|
||||||
validPass = user.comparePassword(password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// code to prevent brute force attack
|
|
||||||
|
|
||||||
const rateLimiter = RateLimiter.getInstance()
|
|
||||||
|
|
||||||
if (!validPass) {
|
|
||||||
const retrySecs = await rateLimiter.consume(
|
|
||||||
req.ip || 'unknown',
|
|
||||||
user?.username
|
|
||||||
)
|
|
||||||
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) throw errors.userNotFound
|
|
||||||
if (!validPass) throw errors.invalidPassword
|
|
||||||
|
|
||||||
// Reset on successful authorization
|
|
||||||
rateLimiter.resetOnSuccess(req.ip || 'unknown', user.username)
|
|
||||||
|
|
||||||
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,
|
|
||||||
needsToUpdatePassword: user.needsToUpdatePassword
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loggedIn: true,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
displayName: user.displayName,
|
|
||||||
isAdmin: user.isAdmin,
|
|
||||||
needsToUpdatePassword: user.needsToUpdatePassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = {
|
|
||||||
invalidPassword: {
|
|
||||||
code: 401,
|
|
||||||
message: 'Invalid Password.'
|
|
||||||
},
|
|
||||||
userNotFound: {
|
|
||||||
code: 401,
|
|
||||||
message: 'Username is not found.'
|
|
||||||
},
|
|
||||||
tooManyRequests: (seconds: number) => ({
|
|
||||||
code: 429,
|
|
||||||
message: `Too Many Requests! Retry after ${convertSecondsToHms(seconds)}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,101 +1,57 @@
|
|||||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { csrfProtection } from './'
|
import { verifyTokenInDB } from '../utils'
|
||||||
import {
|
|
||||||
fetchLatestAutoExec,
|
|
||||||
ModeType,
|
|
||||||
verifyTokenInDB,
|
|
||||||
isAuthorizingRoute,
|
|
||||||
isPublicRoute,
|
|
||||||
publicUser
|
|
||||||
} from '../utils'
|
|
||||||
import { desktopUser } from './desktop'
|
|
||||||
import { authorize } from './authorize'
|
|
||||||
|
|
||||||
export const authenticateAccessToken: RequestHandler = async (
|
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
||||||
req,
|
authenticateToken(
|
||||||
res,
|
|
||||||
next
|
|
||||||
) => {
|
|
||||||
const { MODE } = process.env
|
|
||||||
if (MODE === ModeType.Desktop) {
|
|
||||||
req.user = desktopUser
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextFunction = isAuthorizingRoute(req)
|
|
||||||
? () => authorize(req, res, next)
|
|
||||||
: 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, nextFunction)
|
|
||||||
} else return res.status(401).send('Unauthorized')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.status(401).send('Unauthorized')
|
|
||||||
}
|
|
||||||
|
|
||||||
await authenticateToken(
|
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
nextFunction,
|
next,
|
||||||
process.secrets.ACCESS_TOKEN_SECRET,
|
process.env.ACCESS_TOKEN_SECRET as string,
|
||||||
'accessToken'
|
'accessToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateRefreshToken: RequestHandler = async (
|
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
||||||
req,
|
authenticateToken(
|
||||||
res,
|
|
||||||
next
|
|
||||||
) => {
|
|
||||||
await authenticateToken(
|
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
process.secrets.REFRESH_TOKEN_SECRET,
|
process.env.REFRESH_TOKEN_SECRET as string,
|
||||||
'refreshToken'
|
'refreshToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticateToken = async (
|
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 === ModeType.Desktop) {
|
if (MODE?.trim() !== 'server') {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: 1234,
|
userId: '1234',
|
||||||
clientId: 'desktopModeClientId',
|
clientId: 'desktopModeClientId',
|
||||||
username: 'desktopModeUsername',
|
username: 'desktopModeUsername',
|
||||||
displayName: 'desktopModeDisplayName',
|
displayName: 'desktopModeDisplayName',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true,
|
isActive: true
|
||||||
needsToUpdatePassword: false
|
|
||||||
}
|
}
|
||||||
req.accessToken = 'desktopModeAccessToken'
|
req.accessToken = 'desktopModeAccessToken'
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
try {
|
jwt.verify(token, key, async (err: any, data: any) => {
|
||||||
if (!token) throw 'Unauthorized'
|
if (err) return res.sendStatus(401)
|
||||||
|
|
||||||
const data: any = jwt.verify(token, key)
|
|
||||||
|
|
||||||
|
// verify this valid token's entry in DB
|
||||||
const user = await verifyTokenInDB(
|
const user = await verifyTokenInDB(
|
||||||
data?.userId,
|
data?.userId,
|
||||||
data?.clientId,
|
data?.clientId,
|
||||||
@@ -108,16 +64,8 @@ const authenticateToken = async (
|
|||||||
req.user = user
|
req.user = user
|
||||||
if (tokenType === 'accessToken') req.accessToken = token
|
if (tokenType === 'accessToken') req.accessToken = token
|
||||||
return next()
|
return next()
|
||||||
} else throw 'Unauthorized'
|
} else return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
return res.sendStatus(401)
|
||||||
throw 'Unauthorized'
|
})
|
||||||
} catch (error) {
|
|
||||||
if (await isPublicRoute(req)) {
|
|
||||||
req.user = publicUser
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(401).send('Unauthorized')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
import { RequestHandler } from 'express'
|
|
||||||
import User from '../model/User'
|
|
||||||
import Permission from '../model/Permission'
|
|
||||||
import {
|
|
||||||
PermissionSettingForRoute,
|
|
||||||
PermissionType
|
|
||||||
} from '../controllers/permission'
|
|
||||||
import { getPath, isPublicRoute, TopLevelRoutes } from '../utils'
|
|
||||||
|
|
||||||
export const authorize: RequestHandler = async (req, res, next) => {
|
|
||||||
const { user } = req
|
|
||||||
|
|
||||||
if (!user) return res.sendStatus(401)
|
|
||||||
|
|
||||||
// no need to check for permissions when user is admin
|
|
||||||
if (user.isAdmin) return next()
|
|
||||||
|
|
||||||
// no need to check for permissions when route is Public
|
|
||||||
if (await isPublicRoute(req)) return next()
|
|
||||||
|
|
||||||
const dbUser = await User.findOne({ id: user.userId })
|
|
||||||
if (!dbUser) return res.sendStatus(401)
|
|
||||||
|
|
||||||
const path = getPath(req)
|
|
||||||
const { baseUrl } = req
|
|
||||||
const topLevelRoute =
|
|
||||||
TopLevelRoutes.find((route) => baseUrl.startsWith(route)) || baseUrl
|
|
||||||
|
|
||||||
// find permission w.r.t user
|
|
||||||
const permission = await Permission.findOne({
|
|
||||||
path,
|
|
||||||
type: PermissionType.route,
|
|
||||||
user: dbUser._id
|
|
||||||
})
|
|
||||||
|
|
||||||
if (permission) {
|
|
||||||
if (permission.setting === PermissionSettingForRoute.grant) return next()
|
|
||||||
else return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
// find permission w.r.t user on top level
|
|
||||||
const topLevelPermission = await Permission.findOne({
|
|
||||||
path: topLevelRoute,
|
|
||||||
type: PermissionType.route,
|
|
||||||
user: dbUser._id
|
|
||||||
})
|
|
||||||
|
|
||||||
if (topLevelPermission) {
|
|
||||||
if (topLevelPermission.setting === PermissionSettingForRoute.grant)
|
|
||||||
return next()
|
|
||||||
else return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
let isPermissionDenied = false
|
|
||||||
|
|
||||||
// find permission w.r.t user's groups
|
|
||||||
for (const group of dbUser.groups) {
|
|
||||||
const groupPermission = await Permission.findOne({
|
|
||||||
path,
|
|
||||||
type: PermissionType.route,
|
|
||||||
group
|
|
||||||
})
|
|
||||||
|
|
||||||
if (groupPermission) {
|
|
||||||
if (groupPermission.setting === PermissionSettingForRoute.grant) {
|
|
||||||
return next()
|
|
||||||
} else {
|
|
||||||
isPermissionDenied = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPermissionDenied) {
|
|
||||||
// find permission w.r.t user's groups on top level
|
|
||||||
for (const group of dbUser.groups) {
|
|
||||||
const groupPermission = await Permission.findOne({
|
|
||||||
path: topLevelRoute,
|
|
||||||
type: PermissionType.route,
|
|
||||||
group
|
|
||||||
})
|
|
||||||
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { RequestHandler } from 'express'
|
|
||||||
import { convertSecondsToHms } from '@sasjs/utils'
|
|
||||||
import { RateLimiter } from '../utils'
|
|
||||||
|
|
||||||
export const bruteForceProtection: RequestHandler = async (req, res, next) => {
|
|
||||||
const ip = req.ip || 'unknown'
|
|
||||||
const username = req.body.username
|
|
||||||
|
|
||||||
const rateLimiter = RateLimiter.getInstance()
|
|
||||||
|
|
||||||
const retrySecs = await rateLimiter.check(ip, username)
|
|
||||||
|
|
||||||
if (retrySecs > 0) {
|
|
||||||
res
|
|
||||||
.status(429)
|
|
||||||
.send(`Too Many Requests! Retry after ${convertSecondsToHms(retrySecs)}`)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { RequestHandler } from 'express'
|
|
||||||
import csrf from 'csrf'
|
|
||||||
|
|
||||||
const csrfTokens = new csrf()
|
|
||||||
const secret = csrfTokens.secretSync()
|
|
||||||
|
|
||||||
export const generateCSRFToken = () => csrfTokens.create(secret)
|
|
||||||
|
|
||||||
export const csrfProtection: RequestHandler = (req, res, next) => {
|
|
||||||
if (req.method === 'GET') return next()
|
|
||||||
|
|
||||||
// Reads the token from the following locations, in order:
|
|
||||||
// req.body.csrf_token - typically generated by the body-parser module.
|
|
||||||
// req.query.csrf_token - a built-in from Express.js to read from the URL query string.
|
|
||||||
// req.headers['csrf-token'] - the CSRF-Token HTTP request header.
|
|
||||||
// req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
|
|
||||||
// req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
|
|
||||||
// req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.
|
|
||||||
|
|
||||||
const token =
|
|
||||||
req.body?.csrf_token ||
|
|
||||||
req.query?.csrf_token ||
|
|
||||||
req.headers['csrf-token'] ||
|
|
||||||
req.headers['xsrf-token'] ||
|
|
||||||
req.headers['x-csrf-token'] ||
|
|
||||||
req.headers['x-xsrf-token']
|
|
||||||
|
|
||||||
if (!csrfTokens.verify(secret, token)) {
|
|
||||||
return res.status(400).send('Invalid CSRF token!')
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,18 @@
|
|||||||
import { RequestHandler, Request } from 'express'
|
export const desktopRestrict = (req: any, res: any, next: any) => {
|
||||||
import { userInfo } from 'os'
|
|
||||||
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) {
|
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||||
if (!reqAllowedInDesktopMode(req))
|
|
||||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
export const desktopUsername = (req: any, res: any, next: any) => {
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server')
|
||||||
|
return res.status(200).send({
|
||||||
|
userId: 12345,
|
||||||
|
username: 'DESKTOPusername',
|
||||||
|
displayName: 'DESKTOP User'
|
||||||
|
})
|
||||||
|
|
||||||
export const desktopUser: RequestUser = {
|
next()
|
||||||
userId: 12345,
|
|
||||||
clientId: 'desktop_app',
|
|
||||||
username: userInfo().username,
|
|
||||||
displayName: userInfo().username,
|
|
||||||
isAdmin: true,
|
|
||||||
isActive: true,
|
|
||||||
needsToUpdatePassword: false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
export * from './authenticateToken'
|
export * from './authenticateToken'
|
||||||
export * from './authorize'
|
|
||||||
export * from './csrfProtection'
|
|
||||||
export * from './desktop'
|
export * from './desktop'
|
||||||
export * from './verifyAdmin'
|
export * from './verifyAdmin'
|
||||||
export * from './verifyAdminIfNeeded'
|
export * from './verifyAdminIfNeeded'
|
||||||
export * from './bruteForceProtection'
|
|
||||||
|
|||||||
@@ -1,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,9 +1,6 @@
|
|||||||
import { RequestHandler } from 'express'
|
export const verifyAdmin = (req: any, res: any, next: any) => {
|
||||||
import { ModeType } from '../utils'
|
|
||||||
|
|
||||||
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE === ModeType.Desktop) return next()
|
if (MODE?.trim() !== 'server') return next()
|
||||||
|
|
||||||
const { user } = req
|
const { user } = req
|
||||||
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
||||||
|
|||||||
@@ -1,22 +1,9 @@
|
|||||||
import { RequestHandler } from 'express'
|
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
|
||||||
|
|
||||||
// This middleware checks if a non-admin user trying to
|
|
||||||
// access information of other user
|
|
||||||
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
|
|
||||||
const { user } = req
|
const { user } = req
|
||||||
|
const userId = parseInt(req.params.userId)
|
||||||
|
|
||||||
if (!user?.isAdmin) {
|
if (!user.isAdmin && user.userId !== userId) {
|
||||||
let adminAccountRequired: boolean = true
|
return res.status(401).send('Admin account required')
|
||||||
|
|
||||||
if (req.params.userId) {
|
|
||||||
adminAccountRequired = user?.userId !== parseInt(req.params.userId)
|
|
||||||
} else if (req.params.username) {
|
|
||||||
adminAccountRequired = user?.username !== req.params.username
|
|
||||||
}
|
|
||||||
|
|
||||||
if (adminAccountRequired)
|
|
||||||
return res.status(401).send('Admin account required')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import mongoose, { Schema } from 'mongoose'
|
import mongoose, { Schema } from 'mongoose'
|
||||||
|
|
||||||
export const NUMBER_OF_SECONDS_IN_A_DAY = 86400
|
|
||||||
export interface ClientPayload {
|
export interface ClientPayload {
|
||||||
/**
|
/**
|
||||||
* Client ID
|
* Client ID
|
||||||
@@ -12,16 +11,6 @@ export interface ClientPayload {
|
|||||||
* @example "someRandomCryptoString"
|
* @example "someRandomCryptoString"
|
||||||
*/
|
*/
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
/**
|
|
||||||
* Number of seconds after which access token will expire. Default is 86400 (1 day)
|
|
||||||
* @example 86400
|
|
||||||
*/
|
|
||||||
accessTokenExpiration?: number
|
|
||||||
/**
|
|
||||||
* Number of seconds after which access token will expire. Default is 2592000 (30 days)
|
|
||||||
* @example 2592000
|
|
||||||
*/
|
|
||||||
refreshTokenExpiration?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientSchema = new Schema<ClientPayload>({
|
const ClientSchema = new Schema<ClientPayload>({
|
||||||
@@ -32,14 +21,6 @@ const ClientSchema = new Schema<ClientPayload>({
|
|||||||
clientSecret: {
|
clientSecret: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
|
||||||
accessTokenExpiration: {
|
|
||||||
type: Number,
|
|
||||||
default: NUMBER_OF_SECONDS_IN_A_DAY
|
|
||||||
},
|
|
||||||
refreshTokenExpiration: {
|
|
||||||
type: Number,
|
|
||||||
default: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import mongoose, { Schema } from 'mongoose'
|
|
||||||
|
|
||||||
export interface ConfigurationType {
|
|
||||||
/**
|
|
||||||
* SecretOrPrivateKey to sign Access Token
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
ACCESS_TOKEN_SECRET: string
|
|
||||||
/**
|
|
||||||
* SecretOrPrivateKey to sign Refresh Token
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
REFRESH_TOKEN_SECRET: string
|
|
||||||
/**
|
|
||||||
* SecretOrPrivateKey to sign Auth Code
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
AUTH_CODE_SECRET: string
|
|
||||||
/**
|
|
||||||
* Secret used to sign the session cookie
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
SESSION_SECRET: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigurationSchema = new Schema<ConfigurationType>({
|
|
||||||
ACCESS_TOKEN_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
REFRESH_TOKEN_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
AUTH_CODE_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
SESSION_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default mongoose.model('Configuration', ConfigurationSchema)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import mongoose, { Schema } from 'mongoose'
|
|
||||||
|
|
||||||
const CounterSchema = new Schema({
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
seq: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default mongoose.model('Counter', CounterSchema)
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
import { GroupDetailsResponse } from '../controllers'
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
import User, { IUser } from './User'
|
|
||||||
import { AuthProviderType, getSequenceNextValue } from '../utils'
|
|
||||||
|
|
||||||
export const PUBLIC_GROUP_NAME = 'Public'
|
|
||||||
|
|
||||||
export interface GroupPayload {
|
export interface GroupPayload {
|
||||||
/**
|
/**
|
||||||
@@ -27,91 +23,61 @@ interface IGroupDocument extends GroupPayload, Document {
|
|||||||
groupId: number
|
groupId: number
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
users: Schema.Types.ObjectId[]
|
users: Schema.Types.ObjectId[]
|
||||||
authProvider?: AuthProviderType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGroup extends IGroupDocument {
|
interface IGroup extends IGroupDocument {
|
||||||
addUser(user: IUser): Promise<GroupDetailsResponse>
|
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
||||||
removeUser(user: IUser): Promise<GroupDetailsResponse>
|
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
||||||
hasUser(user: IUser): boolean
|
|
||||||
}
|
}
|
||||||
interface IGroupModel extends Model<IGroup> {}
|
interface IGroupModel extends Model<IGroup> {}
|
||||||
|
|
||||||
const groupSchema = new Schema<IGroupDocument>({
|
const groupSchema = new Schema<IGroupDocument>({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
groupId: {
|
|
||||||
type: Number,
|
|
||||||
unique: true
|
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Group description.'
|
default: 'Group description.'
|
||||||
},
|
},
|
||||||
authProvider: {
|
|
||||||
type: String,
|
|
||||||
enum: AuthProviderType
|
|
||||||
},
|
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
|
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
|
||||||
})
|
})
|
||||||
|
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
groupSchema.pre('save', async function () {
|
|
||||||
if (this.isNew) {
|
|
||||||
this.groupId = await getSequenceNextValue('groupId')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
groupSchema.post('save', function (group: IGroup, next: Function) {
|
groupSchema.post('save', function (group: IGroup, next: Function) {
|
||||||
group.populate('users', 'id username displayName -_id').then(function () {
|
group.populate('users', 'id username displayName -_id').then(function () {
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// pre remove hook to remove all references of group from users
|
|
||||||
groupSchema.pre('remove', async function (this: IGroupDocument) {
|
|
||||||
const userIds = this.users
|
|
||||||
await Promise.all(
|
|
||||||
userIds.map(async (userId) => {
|
|
||||||
const user = await User.findById(userId)
|
|
||||||
user?.removeGroup(this._id)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Instance Methods
|
// Instance Methods
|
||||||
groupSchema.method('addUser', async function (user: IUser) {
|
groupSchema.method(
|
||||||
const userObjectId = user._id
|
'addUser',
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
async function (userObjectId: Schema.Types.ObjectId) {
|
||||||
if (userIdIndex === -1) {
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
this.users.push(userObjectId)
|
if (userIdIndex === -1) {
|
||||||
user.addGroup(this._id)
|
this.users.push(userObjectId)
|
||||||
|
}
|
||||||
|
this.markModified('users')
|
||||||
|
return this.save()
|
||||||
}
|
}
|
||||||
this.markModified('users')
|
)
|
||||||
return this.save()
|
groupSchema.method(
|
||||||
})
|
'removeUser',
|
||||||
groupSchema.method('removeUser', async function (user: IUser) {
|
async function (userObjectId: Schema.Types.ObjectId) {
|
||||||
const userObjectId = user._id
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
if (userIdIndex > -1) {
|
||||||
if (userIdIndex > -1) {
|
this.users.splice(userIdIndex, 1)
|
||||||
this.users.splice(userIdIndex, 1)
|
}
|
||||||
user.removeGroup(this._id)
|
this.markModified('users')
|
||||||
|
return this.save()
|
||||||
}
|
}
|
||||||
this.markModified('users')
|
)
|
||||||
return this.save()
|
|
||||||
})
|
|
||||||
groupSchema.method('hasUser', function (user: IUser) {
|
|
||||||
const userObjectId = user._id
|
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
|
||||||
return userIdIndex > -1
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
||||||
'Group',
|
'Group',
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { Schema, model, Document, Model } from 'mongoose'
|
|
||||||
import { PermissionDetailsResponse } from '../controllers'
|
|
||||||
import { getSequenceNextValue } from '../utils'
|
|
||||||
|
|
||||||
interface GetPermissionBy {
|
|
||||||
user?: Schema.Types.ObjectId
|
|
||||||
group?: Schema.Types.ObjectId
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPermissionDocument extends Document {
|
|
||||||
path: string
|
|
||||||
type: string
|
|
||||||
setting: string
|
|
||||||
permissionId: number
|
|
||||||
user: Schema.Types.ObjectId
|
|
||||||
group: Schema.Types.ObjectId
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPermission extends IPermissionDocument {}
|
|
||||||
|
|
||||||
interface IPermissionModel extends Model<IPermission> {
|
|
||||||
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
const permissionSchema = new Schema<IPermissionDocument>({
|
|
||||||
permissionId: {
|
|
||||||
type: Number,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
setting: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
user: { type: Schema.Types.ObjectId, ref: 'User' },
|
|
||||||
group: { type: Schema.Types.ObjectId, ref: 'Group' }
|
|
||||||
})
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
permissionSchema.pre('save', async function () {
|
|
||||||
if (this.isNew) {
|
|
||||||
this.permissionId = await getSequenceNextValue('permissionId')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Static Methods
|
|
||||||
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
|
|
||||||
PermissionDetailsResponse[]
|
|
||||||
> {
|
|
||||||
return (await this.find(getBy)
|
|
||||||
.select({
|
|
||||||
_id: 0,
|
|
||||||
permissionId: 1,
|
|
||||||
path: 1,
|
|
||||||
type: 1,
|
|
||||||
setting: 1
|
|
||||||
})
|
|
||||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
|
||||||
.populate({
|
|
||||||
path: 'group',
|
|
||||||
select: 'groupId name description -_id',
|
|
||||||
populate: {
|
|
||||||
path: 'users',
|
|
||||||
select: 'id username displayName isAdmin -_id',
|
|
||||||
options: { limit: 15 }
|
|
||||||
}
|
|
||||||
})) as unknown as PermissionDetailsResponse[]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Permission: IPermissionModel = model<
|
|
||||||
IPermission,
|
|
||||||
IPermissionModel
|
|
||||||
>('Permission', permissionSchema)
|
|
||||||
|
|
||||||
export default Permission
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import { AuthProviderType, getSequenceNextValue } from '../utils'
|
|
||||||
|
|
||||||
export interface UserPayload {
|
export interface UserPayload {
|
||||||
/**
|
/**
|
||||||
@@ -27,29 +27,18 @@ 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: Schema.Types.ObjectId
|
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
needsToUpdatePassword: boolean
|
|
||||||
autoExec: string
|
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
authProvider?: AuthProviderType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser extends IUserDocument {
|
interface IUser extends IUserDocument {
|
||||||
comparePassword(password: string): boolean
|
comparePassword(password: string): boolean
|
||||||
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
|
||||||
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
|
||||||
}
|
}
|
||||||
interface IUserModel extends Model<IUser> {
|
interface IUserModel extends Model<IUser> {
|
||||||
hashPassword(password: string): string
|
hashPassword(password: string): string
|
||||||
@@ -65,18 +54,10 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
required: true,
|
required: true,
|
||||||
unique: true
|
unique: true
|
||||||
},
|
},
|
||||||
id: {
|
|
||||||
type: Number,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
authProvider: {
|
|
||||||
type: String,
|
|
||||||
enum: AuthProviderType
|
|
||||||
},
|
|
||||||
isAdmin: {
|
isAdmin: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -85,13 +66,6 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
needsToUpdatePassword: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
autoExec: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
||||||
tokens: [
|
tokens: [
|
||||||
{
|
{
|
||||||
@@ -110,15 +84,7 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
|
||||||
// Hooks
|
|
||||||
userSchema.pre('save', async function (next) {
|
|
||||||
if (this.isNew) {
|
|
||||||
this.id = await getSequenceNextValue('id')
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Static Methods
|
// Static Methods
|
||||||
userSchema.static('hashPassword', (password: string): string => {
|
userSchema.static('hashPassword', (password: string): string => {
|
||||||
@@ -131,28 +97,6 @@ userSchema.method('comparePassword', function (password: string): boolean {
|
|||||||
if (bcrypt.compareSync(password, this.password)) return true
|
if (bcrypt.compareSync(password, this.password)) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
userSchema.method(
|
|
||||||
'addGroup',
|
|
||||||
async function (groupObjectId: Schema.Types.ObjectId) {
|
|
||||||
const groupIdIndex = this.groups.indexOf(groupObjectId)
|
|
||||||
if (groupIdIndex === -1) {
|
|
||||||
this.groups.push(groupObjectId)
|
|
||||||
}
|
|
||||||
this.markModified('groups')
|
|
||||||
return this.save()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
userSchema.method(
|
|
||||||
'removeGroup',
|
|
||||||
async function (groupObjectId: Schema.Types.ObjectId) {
|
|
||||||
const groupIdIndex = this.groups.indexOf(groupObjectId)
|
|
||||||
if (groupIdIndex > -1) {
|
|
||||||
this.groups.splice(groupIdIndex, 1)
|
|
||||||
}
|
|
||||||
this.markModified('groups')
|
|
||||||
return this.save()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)
|
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +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 { tokenValidation, updatePasswordValidation } 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.patch(
|
const clientIDs = new Set()
|
||||||
'/updatePassword',
|
|
||||||
authenticateAccessToken,
|
|
||||||
async (req, res) => {
|
|
||||||
const { error, value: body } = updatePasswordValidation(req.body)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
export const populateClients = async () => {
|
||||||
await controller.updatePassword(req, body)
|
const result = await Client.find()
|
||||||
res.sendStatus(204)
|
clientIDs.clear()
|
||||||
} catch (err: any) {
|
result.forEach((r) => {
|
||||||
res.status(err.code).send(err.message)
|
clientIDs.add(r.clientId)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
authRouter.post('/token', async (req, res) => {
|
authRouter.post('/authorize', async (req, res) => {
|
||||||
const { error, value: body } = tokenValidation(req.body)
|
const { error, value: body } = authorizeValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
const { clientId } = body
|
||||||
|
|
||||||
|
// Verify client ID
|
||||||
|
if (!clientIDs.has(clientId)) {
|
||||||
|
return res.status(403).send('Invalid clientId.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AuthController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.token(body)
|
const response = await controller.authorize(body)
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -42,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)
|
||||||
|
|
||||||
@@ -57,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) {}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { AuthConfigController } from '../../controllers'
|
|
||||||
const authConfigRouter = express.Router()
|
|
||||||
|
|
||||||
authConfigRouter.get('/', async (req, res) => {
|
|
||||||
const controller = new AuthConfigController()
|
|
||||||
try {
|
|
||||||
const response = controller.getDetail()
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(500).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
authConfigRouter.post('/synchroniseWithLDAP', async (req, res) => {
|
|
||||||
const controller = new AuthConfigController()
|
|
||||||
try {
|
|
||||||
const response = await controller.synchroniseWithLDAP()
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(500).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default authConfigRouter
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { ClientController } from '../../controllers'
|
import { ClientController } from '../../controllers'
|
||||||
import { registerClientValidation } from '../../utils'
|
import { registerClientValidation } from '../../utils'
|
||||||
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
|
||||||
|
|
||||||
const clientRouter = express.Router()
|
const clientRouter = express.Router()
|
||||||
|
|
||||||
@@ -18,19 +17,4 @@ clientRouter.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
clientRouter.get(
|
|
||||||
'/',
|
|
||||||
authenticateAccessToken,
|
|
||||||
verifyAdmin,
|
|
||||||
async (req, res) => {
|
|
||||||
const controller = new ClientController()
|
|
||||||
try {
|
|
||||||
const response = await controller.getAllClients()
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default clientRouter
|
export default clientRouter
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { runCodeValidation, triggerCodeValidation } from '../../utils'
|
import { runSASValidation } from '../../utils'
|
||||||
import { CodeController } from '../../controllers/'
|
import { CodeController } from '../../controllers/'
|
||||||
|
|
||||||
const runRouter = express.Router()
|
const runRouter = express.Router()
|
||||||
@@ -7,11 +7,11 @@ const runRouter = express.Router()
|
|||||||
const controller = new CodeController()
|
const controller = new CodeController()
|
||||||
|
|
||||||
runRouter.post('/execute', async (req, res) => {
|
runRouter.post('/execute', async (req, res) => {
|
||||||
const { error, value: body } = runCodeValidation(req.body)
|
const { error, value: body } = runSASValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeCode(req, body)
|
const response = await controller.executeSASCode(req, body)
|
||||||
|
|
||||||
if (response instanceof Buffer) {
|
if (response instanceof Buffer) {
|
||||||
res.writeHead(200, (req as any).sasHeaders)
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
@@ -28,22 +28,4 @@ runRouter.post('/execute', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
runRouter.post('/trigger', async (req, res) => {
|
|
||||||
const { error, value: body } = triggerCodeValidation(req.body)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.triggerCode(req, body)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default runRouter
|
export default runRouter
|
||||||
|
|||||||
@@ -7,14 +7,9 @@ import { multerSingle } from '../../middlewares/multer'
|
|||||||
import { DriveController } from '../../controllers/'
|
import { DriveController } from '../../controllers/'
|
||||||
import {
|
import {
|
||||||
deployValidation,
|
deployValidation,
|
||||||
extractJSONFromZip,
|
|
||||||
extractName,
|
|
||||||
fileBodyValidation,
|
fileBodyValidation,
|
||||||
fileParamValidation,
|
fileParamValidation,
|
||||||
folderBodyValidation,
|
folderParamValidation
|
||||||
folderParamValidation,
|
|
||||||
isZipFile,
|
|
||||||
renameBodyValidation
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
const controller = new DriveController()
|
const controller = new DriveController()
|
||||||
@@ -54,24 +49,7 @@ driveRouter.post(
|
|||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||||
|
|
||||||
let fileContent: string = ''
|
const fileContent = await readFile(req.file.path)
|
||||||
|
|
||||||
const { value: zipFile } = isZipFile(req.file)
|
|
||||||
if (zipFile) {
|
|
||||||
fileContent = await extractJSONFromZip(zipFile)
|
|
||||||
const fileInZip = extractName(zipFile.originalname)
|
|
||||||
|
|
||||||
if (!fileContent) {
|
|
||||||
deleteFile(req.file.path)
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.send(
|
|
||||||
`No content present in ${fileInZip} of compressed file ${zipFile.originalname}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileContent = await readFile(req.file.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
let jsonContent
|
let jsonContent
|
||||||
try {
|
try {
|
||||||
@@ -121,11 +99,7 @@ driveRouter.get('/file', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await controller.getFile(req, query._filePath)
|
await controller.getFile(req, query._filePath)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -138,11 +112,7 @@ driveRouter.get('/folder', async (req, res) => {
|
|||||||
const response = await controller.getFolder(query._folderPath)
|
const response = await controller.getFolder(query._folderPath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -155,28 +125,7 @@ driveRouter.delete('/file', async (req, res) => {
|
|||||||
const response = await controller.deleteFile(query._filePath)
|
const response = await controller.deleteFile(query._filePath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
driveRouter.delete('/folder', async (req, res) => {
|
|
||||||
const { error: errQ, value: query } = folderParamValidation(req.query, true)
|
|
||||||
|
|
||||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.deleteFolder(query._folderPath)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -203,33 +152,11 @@ driveRouter.post(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
await deleteFile(req.file.path)
|
||||||
|
res.status(403).send(err.toString())
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
driveRouter.post('/folder', async (req, res) => {
|
|
||||||
const { error, value: body } = folderBodyValidation(req.body)
|
|
||||||
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.addFolder(body)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
driveRouter.patch(
|
driveRouter.patch(
|
||||||
'/file',
|
'/file',
|
||||||
(...arg) => multerSingle('file', arg),
|
(...arg) => multerSingle('file', arg),
|
||||||
@@ -253,33 +180,11 @@ driveRouter.patch(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
await deleteFile(req.file.path)
|
||||||
|
res.status(403).send(err.toString())
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
driveRouter.post('/rename', async (req, res) => {
|
|
||||||
const { error, value: body } = renameBodyValidation(req.body)
|
|
||||||
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.rename(body)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
driveRouter.get('/fileTree', async (req, res) => {
|
driveRouter.get('/fileTree', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFileTree()
|
const response = await controller.getFileTree()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { GroupController } from '../../controllers/'
|
import { GroupController } from '../../controllers/'
|
||||||
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
||||||
import { getGroupValidation, registerGroupValidation } from '../../utils'
|
import { registerGroupValidation } from '../../utils'
|
||||||
|
|
||||||
const groupRouter = express.Router()
|
const groupRouter = express.Router()
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ groupRouter.post(
|
|||||||
const response = await controller.createGroup(body)
|
const response = await controller.createGroup(body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -29,57 +29,35 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
const response = await controller.getAllGroups()
|
const response = await controller.getAllGroups()
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
groupRouter.get(
|
|
||||||
'/by/groupname/:name',
|
|
||||||
authenticateAccessToken,
|
|
||||||
async (req, res) => {
|
|
||||||
const { error, value: params } = getGroupValidation(req.params)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
const { name } = params
|
|
||||||
|
|
||||||
const controller = new GroupController()
|
|
||||||
try {
|
|
||||||
const response = await controller.getGroupByGroupName(name)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(err.code).send(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
groupRouter.post(
|
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(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -88,18 +66,15 @@ 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(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -108,15 +83,15 @@ 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(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import swaggerUi from 'swagger-ui-express'
|
|||||||
import {
|
import {
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
desktopRestrict,
|
desktopRestrict,
|
||||||
|
desktopUsername,
|
||||||
verifyAdmin
|
verifyAdmin
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
@@ -17,13 +18,11 @@ import groupRouter from './group'
|
|||||||
import clientRouter from './client'
|
import clientRouter from './client'
|
||||||
import authRouter from './auth'
|
import authRouter from './auth'
|
||||||
import sessionRouter from './session'
|
import sessionRouter from './session'
|
||||||
import permissionRouter from './permission'
|
|
||||||
import authConfigRouter from './authConfig'
|
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.use('/info', infoRouter)
|
router.use('/info', infoRouter)
|
||||||
router.use('/session', authenticateAccessToken, sessionRouter)
|
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||||
router.use('/auth', desktopRestrict, authRouter)
|
router.use('/auth', desktopRestrict, authRouter)
|
||||||
router.use(
|
router.use(
|
||||||
'/client',
|
'/client',
|
||||||
@@ -37,36 +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(
|
|
||||||
'/permission',
|
|
||||||
desktopRestrict,
|
|
||||||
authenticateAccessToken,
|
|
||||||
permissionRouter
|
|
||||||
)
|
|
||||||
|
|
||||||
router.use(
|
|
||||||
'/authConfig',
|
|
||||||
desktopRestrict,
|
|
||||||
authenticateAccessToken,
|
|
||||||
verifyAdmin,
|
|
||||||
authConfigRouter
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,14 +13,4 @@ infoRouter.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
infoRouter.get('/authorizedRoutes', async (req, res) => {
|
|
||||||
const controller = new InfoController()
|
|
||||||
try {
|
|
||||||
const response = controller.authorizedRoutes()
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default infoRouter
|
export default infoRouter
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { PermissionController } from '../../controllers/'
|
|
||||||
import { verifyAdmin } from '../../middlewares'
|
|
||||||
import {
|
|
||||||
registerPermissionValidation,
|
|
||||||
updatePermissionValidation
|
|
||||||
} from '../../utils'
|
|
||||||
|
|
||||||
const permissionRouter = express.Router()
|
|
||||||
const controller = new PermissionController()
|
|
||||||
|
|
||||||
permissionRouter.get('/', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await controller.getAllPermissions(req)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
permissionRouter.post('/', verifyAdmin, async (req, res) => {
|
|
||||||
const { error, value: body } = registerPermissionValidation(req.body)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.createPermission(body)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
|
|
||||||
const { permissionId } = req.params
|
|
||||||
|
|
||||||
const { error, value: body } = updatePermissionValidation(req.body)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.updatePermission(permissionId, body)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
permissionRouter.delete(
|
|
||||||
'/:permissionId',
|
|
||||||
verifyAdmin,
|
|
||||||
async (req: any, res) => {
|
|
||||||
const { permissionId } = req.params
|
|
||||||
|
|
||||||
try {
|
|
||||||
await controller.deletePermission(permissionId)
|
|
||||||
res.status(200).send('Permission Deleted!')
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
export default permissionRouter
|
|
||||||
@@ -1,37 +1,16 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { SessionController } from '../../controllers'
|
import { SessionController } from '../../controllers'
|
||||||
import { sessionIdValidation } from '../../utils'
|
|
||||||
|
|
||||||
const sessionRouter = express.Router()
|
const sessionRouter = express.Router()
|
||||||
|
|
||||||
const controller = new SessionController()
|
|
||||||
|
|
||||||
sessionRouter.get('/', async (req, res) => {
|
sessionRouter.get('/', async (req, res) => {
|
||||||
|
const controller = new SessionController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.session(req)
|
const response = await controller.session(req)
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sessionRouter.get('/:sessionId/state', async (req, res) => {
|
|
||||||
const { error, value: params } = sessionIdValidation(req.params)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.sessionState(params.sessionId)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default sessionRouter
|
export default sessionRouter
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import request from 'supertest'
|
|||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, ClientController } from '../../../controllers/'
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../../../model/Client'
|
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
clientId: 'someclientID',
|
clientId: 'someclientID',
|
||||||
@@ -27,7 +26,6 @@ describe('client', () => {
|
|||||||
let app: Express
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let adminAccessToken: string
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const clientController = new ClientController()
|
const clientController = new ClientController()
|
||||||
|
|
||||||
@@ -36,18 +34,6 @@ describe('client', () => {
|
|||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
const dbUser = await userController.createUser(adminUser)
|
|
||||||
adminAccessToken = generateAccessToken({
|
|
||||||
clientId: client.clientId,
|
|
||||||
userId: dbUser.id
|
|
||||||
})
|
|
||||||
await saveTokensInDB(
|
|
||||||
dbUser.id,
|
|
||||||
client.clientId,
|
|
||||||
adminAccessToken,
|
|
||||||
'refreshToken'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -57,6 +43,22 @@ describe('client', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
|
let adminAccessToken: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const dbUser = await userController.createUser(adminUser)
|
||||||
|
adminAccessToken = generateAccessToken({
|
||||||
|
clientId: client.clientId,
|
||||||
|
userId: dbUser.id
|
||||||
|
})
|
||||||
|
await saveTokensInDB(
|
||||||
|
dbUser.id,
|
||||||
|
client.clientId,
|
||||||
|
adminAccessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const collections = mongoose.connection.collections
|
const collections = mongoose.connection.collections
|
||||||
const collection = collections['clients']
|
const collection = collections['clients']
|
||||||
@@ -155,80 +157,4 @@ describe('client', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('get', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
const collections = mongoose.connection.collections
|
|
||||||
const collection = collections['clients']
|
|
||||||
await collection.deleteMany({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with an array of all clients', async () => {
|
|
||||||
await clientController.createClient(newClient)
|
|
||||||
await clientController.createClient({
|
|
||||||
clientId: 'clientID',
|
|
||||||
clientSecret: 'clientSecret'
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/client')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
clientId: 'newClientID',
|
|
||||||
clientSecret: 'newClientSecret',
|
|
||||||
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
|
||||||
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
|
||||||
},
|
|
||||||
{
|
|
||||||
clientId: 'clientID',
|
|
||||||
clientSecret: 'clientSecret',
|
|
||||||
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
|
||||||
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
expect(res.body).toEqual(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app).get('/SASjsApi/client').send().expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
|
||||||
const user = {
|
|
||||||
displayName: 'User 2',
|
|
||||||
username: 'username2',
|
|
||||||
password: '12345678',
|
|
||||||
isAdmin: false,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
const dbUser = await userController.createUser(user)
|
|
||||||
const accessToken = generateAccessToken({
|
|
||||||
clientId: client.clientId,
|
|
||||||
userId: dbUser.id
|
|
||||||
})
|
|
||||||
await saveTokensInDB(
|
|
||||||
dbUser.id,
|
|
||||||
client.clientId,
|
|
||||||
accessToken,
|
|
||||||
'refreshToken'
|
|
||||||
)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/client')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Express } from 'express'
|
|||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import AdmZip from 'adm-zip'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
folderExists,
|
folderExists,
|
||||||
@@ -22,23 +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 {
|
import { UserController } from '../../../controllers/'
|
||||||
UserController,
|
|
||||||
PermissionController,
|
|
||||||
PermissionType,
|
|
||||||
PermissionSettingForRoute,
|
|
||||||
PrincipalType
|
|
||||||
} 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 = {
|
||||||
@@ -49,18 +42,11 @@ const user = {
|
|||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const permission = {
|
|
||||||
type: PermissionType.route,
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
setting: PermissionSettingForRoute.grant
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('drive', () => {
|
describe('drive', () => {
|
||||||
let app: Express
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
const permissionController = new PermissionController()
|
|
||||||
|
|
||||||
let accessToken: string
|
let accessToken: string
|
||||||
|
|
||||||
@@ -71,32 +57,11 @@ describe('drive', () => {
|
|||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
accessToken = generateAccessToken({
|
||||||
await permissionController.createPermission({
|
clientId,
|
||||||
...permission,
|
userId: dbUser.id
|
||||||
path: '/SASjsApi/drive/deploy',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
path: '/SASjsApi/drive/deploy/upload',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
path: '/SASjsApi/drive/file',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
path: '/SASjsApi/drive/folder',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
path: '/SASjsApi/drive/rename',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
})
|
||||||
|
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -107,52 +72,11 @@ describe('drive', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('deploy', () => {
|
describe('deploy', () => {
|
||||||
const makeRequest = async (payload: any, type: string = 'payload') => {
|
const shouldFailAssertion = async (payload: any) => {
|
||||||
const requestUrl =
|
const res = await request(app)
|
||||||
type === 'payload'
|
.post('/SASjsApi/drive/deploy')
|
||||||
? '/SASjsApi/drive/deploy'
|
.auth(accessToken, { type: 'bearer' })
|
||||||
: '/SASjsApi/drive/deploy/upload'
|
.send({ appLoc: '/Public', fileTree: payload })
|
||||||
|
|
||||||
if (type === 'payload') {
|
|
||||||
return await request(app)
|
|
||||||
.post(requestUrl)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ appLoc: '/Public', fileTree: payload })
|
|
||||||
}
|
|
||||||
if (type === 'file') {
|
|
||||||
const deployContents = JSON.stringify({
|
|
||||||
appLoc: '/Public',
|
|
||||||
fileTree: payload
|
|
||||||
})
|
|
||||||
return await request(app)
|
|
||||||
.post(requestUrl)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.attach('file', Buffer.from(deployContents), 'deploy.json')
|
|
||||||
} else {
|
|
||||||
const deployContents = JSON.stringify({
|
|
||||||
appLoc: '/Public',
|
|
||||||
fileTree: payload
|
|
||||||
})
|
|
||||||
const zip = new AdmZip()
|
|
||||||
// add file directly
|
|
||||||
zip.addFile(
|
|
||||||
'deploy.json',
|
|
||||||
Buffer.from(deployContents, 'utf8'),
|
|
||||||
'entry comment goes here'
|
|
||||||
)
|
|
||||||
|
|
||||||
return await request(app)
|
|
||||||
.post(requestUrl)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.attach('file', zip.toBuffer(), 'deploy.json.zip')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldFailAssertion = async (
|
|
||||||
payload: any,
|
|
||||||
type: string = 'payload'
|
|
||||||
) => {
|
|
||||||
const res = await makeRequest(payload, type)
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(400)
|
expect(res.statusCode).toEqual(400)
|
||||||
|
|
||||||
@@ -233,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'
|
||||||
@@ -250,241 +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'))
|
||||||
})
|
|
||||||
|
|
||||||
describe('upload', () => {
|
|
||||||
it('should respond with payload example if valid JSON file was not provided', async () => {
|
|
||||||
await shouldFailAssertion(null, 'file')
|
|
||||||
await shouldFailAssertion(undefined, 'file')
|
|
||||||
await shouldFailAssertion('data', 'file')
|
|
||||||
await shouldFailAssertion({}, 'file')
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
userId: 1,
|
|
||||||
title: 'test is cool'
|
|
||||||
},
|
|
||||||
'file'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
membersWRONG: []
|
|
||||||
},
|
|
||||||
'file'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: {}
|
|
||||||
},
|
|
||||||
'file'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
nameWRONG: 'jobs',
|
|
||||||
type: 'folder',
|
|
||||||
members: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'file'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'jobs',
|
|
||||||
type: 'WRONG',
|
|
||||||
members: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'file'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'jobs',
|
|
||||||
type: 'folder',
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'extract',
|
|
||||||
type: 'folder',
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'makedata1',
|
|
||||||
type: 'service',
|
|
||||||
codeWRONG: '%put Hello World!;'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'file'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should successfully deploy if valid JSON file was provided', async () => {
|
|
||||||
const deployContents = JSON.stringify({
|
|
||||||
appLoc: '/public',
|
|
||||||
fileTree: getTreeExample()
|
|
||||||
})
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/drive/deploy/upload')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.attach('file', Buffer.from(deployContents), 'deploy.json')
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
|
||||||
)
|
|
||||||
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
|
||||||
|
|
||||||
const testJobFolder = path.join(
|
|
||||||
getFilesFolder(),
|
|
||||||
'public',
|
|
||||||
'jobs',
|
|
||||||
'extract'
|
|
||||||
)
|
|
||||||
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
|
|
||||||
|
|
||||||
const exampleService = getExampleService()
|
|
||||||
const testJobFile =
|
|
||||||
path.join(testJobFolder, exampleService.name) + '.sas'
|
|
||||||
|
|
||||||
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
|
||||||
|
|
||||||
await expect(readFile(testJobFile)).resolves.toEqual(
|
|
||||||
exampleService.code
|
|
||||||
)
|
|
||||||
|
|
||||||
await deleteFolder(path.join(getFilesFolder(), 'public'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('upload - zipped', () => {
|
|
||||||
it('should respond with payload example if valid Zipped file was not provided', async () => {
|
|
||||||
await shouldFailAssertion(null, 'zip')
|
|
||||||
await shouldFailAssertion(undefined, 'zip')
|
|
||||||
await shouldFailAssertion('data', 'zip')
|
|
||||||
await shouldFailAssertion({}, 'zip')
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
userId: 1,
|
|
||||||
title: 'test is cool'
|
|
||||||
},
|
|
||||||
'zip'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
membersWRONG: []
|
|
||||||
},
|
|
||||||
'zip'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: {}
|
|
||||||
},
|
|
||||||
'zip'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
nameWRONG: 'jobs',
|
|
||||||
type: 'folder',
|
|
||||||
members: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'zip'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'jobs',
|
|
||||||
type: 'WRONG',
|
|
||||||
members: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'zip'
|
|
||||||
)
|
|
||||||
await shouldFailAssertion(
|
|
||||||
{
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'jobs',
|
|
||||||
type: 'folder',
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'extract',
|
|
||||||
type: 'folder',
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'makedata1',
|
|
||||||
type: 'service',
|
|
||||||
codeWRONG: '%put Hello World!;'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'zip'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should successfully deploy if valid Zipped file was provided', async () => {
|
|
||||||
const deployContents = JSON.stringify({
|
|
||||||
appLoc: '/public',
|
|
||||||
fileTree: getTreeExample()
|
|
||||||
})
|
|
||||||
|
|
||||||
const zip = new AdmZip()
|
|
||||||
// add file directly
|
|
||||||
zip.addFile(
|
|
||||||
'deploy.json',
|
|
||||||
Buffer.from(deployContents, 'utf8'),
|
|
||||||
'entry comment goes here'
|
|
||||||
)
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/drive/deploy/upload')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.attach('file', zip.toBuffer(), 'deploy.json.zip')
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
|
||||||
)
|
|
||||||
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
|
||||||
|
|
||||||
const testJobFolder = path.join(
|
|
||||||
getFilesFolder(),
|
|
||||||
'public',
|
|
||||||
'jobs',
|
|
||||||
'extract'
|
|
||||||
)
|
|
||||||
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
|
|
||||||
|
|
||||||
const exampleService = getExampleService()
|
|
||||||
const testJobFile =
|
|
||||||
path.join(testJobFolder, exampleService.name) + '.sas'
|
|
||||||
|
|
||||||
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
|
||||||
|
|
||||||
await expect(readFile(testJobFile)).resolves.toEqual(
|
|
||||||
exampleService.code
|
|
||||||
)
|
|
||||||
|
|
||||||
await deleteFolder(path.join(getFilesFolder(), 'public'))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -502,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'
|
||||||
@@ -551,129 +241,48 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if folder is not present', async () => {
|
it('should respond with Forbidden if folder is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Folder doesn't exist.`)
|
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if folderPath outside Drive', async () => {
|
it('should respond with Forbidden if folderPath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: '/../path/code.sas' })
|
.query({ _folderPath: '/../path/code.sas' })
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't get folder outside drive.`)
|
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if folderPath is of a file', async () => {
|
it('should respond with Forbidden if folderPath is of a file', async () => {
|
||||||
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)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: filePath })
|
.query({ _folderPath: filePath })
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Not a Folder.')
|
expect(res.text).toEqual('Error: Not a Folder.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('post', () => {
|
|
||||||
const folderApi = '/SASjsApi/drive/folder'
|
|
||||||
const pathToDrive = fileUtilModules.getFilesFolder()
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await deleteFolder(path.join(pathToDrive, 'post'))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create a folder on drive', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(folderApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ folderPath: '/post/folder' })
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
|
||||||
expect(res.body).toEqual({
|
|
||||||
status: 'success'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Conflict if the folder already exists', async () => {
|
|
||||||
await createFolder(path.join(pathToDrive, '/post/folder'))
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post(folderApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ folderPath: '/post/folder' })
|
|
||||||
.expect(409)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`Folder already exists.`)
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(409)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(folderApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ folderPath: '../sample' })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't put folder outside drive.`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
const folderApi = '/SASjsApi/drive/folder'
|
|
||||||
const pathToDrive = fileUtilModules.getFilesFolder()
|
|
||||||
|
|
||||||
it('should delete a folder on drive', async () => {
|
|
||||||
await createFolder(path.join(pathToDrive, 'delete'))
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(folderApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.query({ _folderPath: 'delete' })
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
|
||||||
expect(res.body).toEqual({
|
|
||||||
status: 'success'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found if the folder does not exists', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(folderApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.query({ _folderPath: 'notExists' })
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`Folder doesn't exist.`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(folderApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.query({ _folderPath: '../outsideDrive' })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't delete folder outside drive.`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('file', () => {
|
describe('file', () => {
|
||||||
@@ -719,12 +328,12 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Conflict if file is already present', async () => {
|
it('should respond with Forbidden if file is already present', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
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)
|
||||||
@@ -734,13 +343,13 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(409)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('File already exists.')
|
expect(res.text).toEqual('Error: File already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -749,9 +358,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't put file outside drive.`)
|
expect(res.text).toEqual('Error: Cannot put file outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -836,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)
|
||||||
@@ -858,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)
|
||||||
@@ -886,19 +495,19 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if file is not present', async () => {
|
it('should respond with Forbidden if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/drive/file')
|
.patch('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', `/my/path/code-3.sas`)
|
.field('filePath', `/my/path/code-3.sas`)
|
||||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`File doesn't exist.`)
|
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -907,9 +516,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't modify file outside drive.`)
|
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -994,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)
|
||||||
@@ -1014,25 +626,25 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if file is not present', async () => {
|
it('should respond with Forbidden if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: `/my/path/code-4.sas` })
|
.query({ _filePath: `/my/path/code-4.sas` })
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`File doesn't exist.`)
|
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: '/../path/code.sas' })
|
.query({ _filePath: '/../path/code.sas' })
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't get file outside drive.`)
|
expect(res.text).toEqual('Error: Cannot get file outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1058,150 +670,8 @@ describe('drive', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('rename', () => {
|
|
||||||
const renameApi = '/SASjsApi/drive/rename'
|
|
||||||
const pathToDrive = fileUtilModules.getFilesFolder()
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await deleteFolder(path.join(pathToDrive, 'rename'))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should rename a folder', async () => {
|
|
||||||
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: '/rename/folder', newPath: '/rename/renamed' })
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
|
||||||
expect(res.body).toEqual({
|
|
||||||
status: 'success'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should rename a file', async () => {
|
|
||||||
await createFile(
|
|
||||||
path.join(pathToDrive, 'rename', 'file.txt'),
|
|
||||||
'some file content'
|
|
||||||
)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
oldPath: '/rename/file.txt',
|
|
||||||
newPath: '/rename/renamed.txt'
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
|
||||||
expect(res.body).toEqual({
|
|
||||||
status: 'success'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if the oldPath is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ newPath: 'newPath' })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`\"oldPath\" is required`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if the newPath is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: 'oldPath' })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`\"newPath\" is required`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if the oldPath is outside drive', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: '../outside', newPath: 'renamed' })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`Old path can't be outside of drive.`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if the newPath is outside drive', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: 'older', newPath: '../outside' })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`New path can't be outside of drive.`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found if the folder does not exist', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' })
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('No file/folder found for provided path.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Conflict if the folder already exists', async () => {
|
|
||||||
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
|
||||||
await createFolder(path.join(pathToDrive, 'rename', 'exists'))
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: '/rename/folder', newPath: '/rename/exists' })
|
|
||||||
.expect(409)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Folder with new name already exists.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found if the file does not exist', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' })
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('No file/folder found for provided path.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Conflict if the file already exists', async () => {
|
|
||||||
await createFile(
|
|
||||||
path.join(pathToDrive, 'rename', 'file.txt'),
|
|
||||||
'some file content'
|
|
||||||
)
|
|
||||||
await createFile(
|
|
||||||
path.join(pathToDrive, 'rename', 'exists.txt'),
|
|
||||||
'some existing content'
|
|
||||||
)
|
|
||||||
const res = await request(app)
|
|
||||||
.post(renameApi)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' })
|
|
||||||
.expect(409)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('File with new name already exists.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getExampleService = (): ServiceMember =>
|
const getExampleService = (): ServiceMember =>
|
||||||
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
||||||
.members[0] as ServiceMember
|
.members[0] as ServiceMember
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
|
||||||
const adminAccessToken = generateAccessToken({
|
|
||||||
clientId,
|
|
||||||
userId
|
|
||||||
})
|
|
||||||
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
|
||||||
return adminAccessToken
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
|
|||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, GroupController } from '../../../controllers/'
|
import { UserController, GroupController } from '../../../controllers/'
|
||||||
import {
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
generateAccessToken,
|
|
||||||
saveTokensInDB,
|
|
||||||
AuthProviderType
|
|
||||||
} from '../../../utils'
|
|
||||||
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
|
|
||||||
import User from '../../../model/User'
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
@@ -29,16 +23,10 @@ const user = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const group = {
|
const group = {
|
||||||
name: 'dcgroup1',
|
name: 'DCGroup1',
|
||||||
description: 'DC group for testing purposes.'
|
description: 'DC group for testing purposes.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const PUBLIC_GROUP = {
|
|
||||||
name: PUBLIC_GROUP_NAME,
|
|
||||||
description:
|
|
||||||
'A special group that can be used to bypass authentication for particular routes.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const groupController = new GroupController()
|
const groupController = new GroupController()
|
||||||
|
|
||||||
@@ -82,32 +70,6 @@ describe('group', () => {
|
|||||||
expect(res.body.users).toEqual([])
|
expect(res.body.users).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Conflict when group already exists with same name', async () => {
|
|
||||||
await groupController.createGroup(group)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/group')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send(group)
|
|
||||||
.expect(409)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Group name already exists.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request when group name does not match the group name schema', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/group')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ ...group, name: 'Wrong Group Name' })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
'"name" must only contain alpha-numeric characters'
|
|
||||||
)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
const res = await request(app).post('/SASjsApi/group').send().expect(401)
|
const res = await request(app).post('/SASjsApi/group').send().expect(401)
|
||||||
|
|
||||||
@@ -163,51 +125,14 @@ describe('group', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it(`should delete group's reference from users' groups array`, async () => {
|
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
const dbUser1 = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'deletegroup1'
|
|
||||||
})
|
|
||||||
const dbUser2 = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'deletegroup2'
|
|
||||||
})
|
|
||||||
|
|
||||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
|
|
||||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const res1 = await request(app)
|
|
||||||
.get(`/SASjsApi/user/${dbUser1.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res1.body.groups).toEqual([])
|
|
||||||
|
|
||||||
const res2 = await request(app)
|
|
||||||
.get(`/SASjsApi/user/${dbUser2.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res2.body.groups).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.delete(`/SASjsApi/group/1234`)
|
.delete(`/SASjsApi/group/1234`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Group not found.')
|
expect(res.text).toEqual('Error: No Group deleted!')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -291,76 +216,16 @@ describe('group', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/group/1234')
|
.get('/SASjsApi/group/1234')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Group not found.')
|
expect(res.text).toEqual('Error: Group not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('by group name', () => {
|
|
||||||
it('should respond with group', async () => {
|
|
||||||
const { name } = await groupController.createGroup(group)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get(`/SASjsApi/group/by/groupname/${name}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.groupId).toBeTruthy()
|
|
||||||
expect(res.body.name).toEqual(group.name)
|
|
||||||
expect(res.body.description).toEqual(group.description)
|
|
||||||
expect(res.body.isActive).toEqual(true)
|
|
||||||
expect(res.body.users).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with group when access token is not of an admin account', async () => {
|
|
||||||
const accessToken = await generateSaveTokenAndCreateUser({
|
|
||||||
...user,
|
|
||||||
username: 'getbyname' + user.username
|
|
||||||
})
|
|
||||||
|
|
||||||
const { name } = await groupController.createGroup(group)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get(`/SASjsApi/group/by/groupname/${name}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.groupId).toBeTruthy()
|
|
||||||
expect(res.body.name).toEqual(group.name)
|
|
||||||
expect(res.body.description).toEqual(group.description)
|
|
||||||
expect(res.body.isActive).toEqual(true)
|
|
||||||
expect(res.body.users).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/group/by/groupname/dcgroup')
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found if groupname is incorrect', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/group/by/groupname/randomCharacters')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Group not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getAll', () => {
|
describe('getAll', () => {
|
||||||
@@ -380,8 +245,8 @@ describe('group', () => {
|
|||||||
expect(res.body).toEqual([
|
expect(res.body).toEqual([
|
||||||
{
|
{
|
||||||
groupId: expect.anything(),
|
groupId: expect.anything(),
|
||||||
name: group.name,
|
name: 'DCGroup1',
|
||||||
description: group.description
|
description: 'DC group for testing purposes.'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -402,8 +267,8 @@ describe('group', () => {
|
|||||||
expect(res.body).toEqual([
|
expect(res.body).toEqual([
|
||||||
{
|
{
|
||||||
groupId: expect.anything(),
|
groupId: expect.anything(),
|
||||||
name: group.name,
|
name: 'DCGroup1',
|
||||||
description: group.description
|
description: 'DC group for testing purposes.'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -444,34 +309,6 @@ describe('group', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it(`should add group to user's groups array`, async () => {
|
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
const dbUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'addUserToGroup'
|
|
||||||
})
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get(`/SASjsApi/user/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.groups).toEqual([
|
|
||||||
{
|
|
||||||
groupId: expect.anything(),
|
|
||||||
name: group.name,
|
|
||||||
description: group.description
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with group without duplicating user', async () => {
|
it('should respond with group without duplicating user', async () => {
|
||||||
const dbGroup = await groupController.createGroup(group)
|
const dbGroup = await groupController.createGroup(group)
|
||||||
const dbUser = await userController.createUser({
|
const dbUser = await userController.createUser({
|
||||||
@@ -525,86 +362,28 @@ describe('group', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/group/123/123')
|
.post('/SASjsApi/group/123/123')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Group not found.')
|
expect(res.text).toEqual('Error: Group not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if userId is incorrect', async () => {
|
it('should respond with Forbidden if userId is incorrect', async () => {
|
||||||
const dbGroup = await groupController.createGroup(group)
|
const dbGroup = await groupController.createGroup(group)
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
|
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('User not found.')
|
expect(res.text).toEqual('Error: User not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request when adding user to Public group', async () => {
|
|
||||||
const dbGroup = await groupController.createGroup(PUBLIC_GROUP)
|
|
||||||
const dbUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'publicUser'
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
|
|
||||||
const dbGroup = await Group.create({
|
|
||||||
...group,
|
|
||||||
authProvider: AuthProviderType.LDAP
|
|
||||||
})
|
|
||||||
const dbUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'ldapGroupUser'
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(405)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
`Can't add/remove user to group created by external auth provider.`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
|
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
const dbUser = await User.create({
|
|
||||||
...user,
|
|
||||||
username: 'ldapUser',
|
|
||||||
authProvider: AuthProviderType.LDAP
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(405)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
`Can't add/remove user to group created by external auth provider.`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('RemoveUser', () => {
|
describe('RemoveUser', () => {
|
||||||
@@ -633,69 +412,6 @@ describe('group', () => {
|
|||||||
expect(res.body.users).toEqual([])
|
expect(res.body.users).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it(`should remove group from user's groups array`, async () => {
|
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
const dbUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'removeGroupFromUser'
|
|
||||||
})
|
|
||||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get(`/SASjsApi/user/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.groups).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
|
|
||||||
const dbGroup = await Group.create({
|
|
||||||
...group,
|
|
||||||
authProvider: AuthProviderType.LDAP
|
|
||||||
})
|
|
||||||
const dbUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'removeLdapGroupUser'
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(405)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
`Can't add/remove user to group created by external auth provider.`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
|
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
const dbUser = await User.create({
|
|
||||||
...user,
|
|
||||||
username: 'removeLdapUser',
|
|
||||||
authProvider: AuthProviderType.LDAP
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(405)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
`Can't add/remove user to group created by external auth provider.`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.delete('/SASjsApi/group/123/123')
|
.delete('/SASjsApi/group/123/123')
|
||||||
@@ -722,26 +438,26 @@ describe('group', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.delete('/SASjsApi/group/123/123')
|
.delete('/SASjsApi/group/123/123')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Group not found.')
|
expect(res.text).toEqual('Error: Group not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if userId is incorrect', async () => {
|
it('should respond with Forbidden if userId is incorrect', async () => {
|
||||||
const dbGroup = await groupController.createGroup(group)
|
const dbGroup = await groupController.createGroup(group)
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
|
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('User not found.')
|
expect(res.text).toEqual('Error: User not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,596 +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 {
|
|
||||||
DriveController,
|
|
||||||
UserController,
|
|
||||||
GroupController,
|
|
||||||
PermissionController,
|
|
||||||
PrincipalType,
|
|
||||||
PermissionType,
|
|
||||||
PermissionSettingForRoute
|
|
||||||
} from '../../../controllers/'
|
|
||||||
import {
|
|
||||||
UserDetailsResponse,
|
|
||||||
PermissionDetailsResponse
|
|
||||||
} from '../../../controllers'
|
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
|
||||||
|
|
||||||
const deployPayload = {
|
|
||||||
appLoc: 'string',
|
|
||||||
streamWebFolder: 'string',
|
|
||||||
fileTree: {
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'string',
|
|
||||||
type: 'folder',
|
|
||||||
members: [
|
|
||||||
'string',
|
|
||||||
{
|
|
||||||
name: 'string',
|
|
||||||
type: 'service',
|
|
||||||
code: 'string'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
|
||||||
const adminUser = {
|
|
||||||
displayName: 'Test Admin',
|
|
||||||
username: 'testAdminUsername',
|
|
||||||
password: '12345678',
|
|
||||||
isAdmin: true,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
const user = {
|
|
||||||
displayName: 'Test User',
|
|
||||||
username: 'testUsername',
|
|
||||||
password: '87654321',
|
|
||||||
isAdmin: false,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const permission = {
|
|
||||||
path: '/SASjsApi/code/execute',
|
|
||||||
type: PermissionType.route,
|
|
||||||
setting: PermissionSettingForRoute.grant,
|
|
||||||
principalType: PrincipalType.user
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = {
|
|
||||||
name: 'DCGroup1',
|
|
||||||
description: 'DC group for testing purposes.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const userController = new UserController()
|
|
||||||
const groupController = new GroupController()
|
|
||||||
const permissionController = new PermissionController()
|
|
||||||
|
|
||||||
describe('permission', () => {
|
|
||||||
let app: Express
|
|
||||||
let con: Mongoose
|
|
||||||
let mongoServer: MongoMemoryServer
|
|
||||||
let adminAccessToken: string
|
|
||||||
let dbUser: UserDetailsResponse
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await appPromise
|
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
|
||||||
|
|
||||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
|
||||||
dbUser = await userController.createUser(user)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await con.connection.dropDatabase()
|
|
||||||
await con.connection.close()
|
|
||||||
await mongoServer.stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
await deleteAllPermissions()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with new permission when principalType is user', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ ...permission, principalId: dbUser.id })
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
|
||||||
expect(res.body.path).toEqual(permission.path)
|
|
||||||
expect(res.body.type).toEqual(permission.type)
|
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
|
||||||
expect(res.body.user).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with new permission when principalType is group', async () => {
|
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: 'group',
|
|
||||||
principalId: dbGroup.groupId
|
|
||||||
})
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
|
||||||
expect(res.body.path).toEqual(permission.path)
|
|
||||||
expect(res.body.type).toEqual(permission.type)
|
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
|
||||||
expect(res.body.group).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.send(permission)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send(permission)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if path is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
path: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"path" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if path is not valid', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
path: '/some/random/api/endpoint'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if type is not valid', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
type: 'invalid'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('"type" must be [Route]')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if type is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
type: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"type" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
setting: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"setting" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is not valid', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
setting: 'invalid'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if principalType is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"principalType" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if principal type is not valid', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: 'invalid'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('"principalType" must be one of [user, group]')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if principalId is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"principalId" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if principalId is not a number', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: 'someCharacters'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('"principalId" must be a number')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if adding permission for admin user', async () => {
|
|
||||||
const adminUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'adminUser',
|
|
||||||
isAdmin: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: adminUser.id
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Can not add permission for admin user.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found (404) if user is not found', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: 123
|
|
||||||
})
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('User not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found (404) if group is not found', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: 'group',
|
|
||||||
principalId: 123
|
|
||||||
})
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Group not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Conflict (409) if permission already exists', async () => {
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ ...permission, principalId: dbUser.id })
|
|
||||||
.expect(409)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
'Permission already exists with provided Path, Type and User.'
|
|
||||||
)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
let dbPermission: PermissionDetailsResponse | undefined
|
|
||||||
beforeAll(async () => {
|
|
||||||
dbPermission = await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await deleteAllPermissions()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with updated permission', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ setting: PermissionSettingForRoute.deny })
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.setting).toEqual('Deny')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
|
||||||
const accessToken = await generateSaveTokenAndCreateUser({
|
|
||||||
...user,
|
|
||||||
username: 'update' + user.username
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"setting" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is invalid', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
setting: 'invalid'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with not found (404) if permission with provided id does not exist', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch('/SASjsApi/permission/123')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
setting: PermissionSettingForRoute.deny
|
|
||||||
})
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Permission not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should delete permission', async () => {
|
|
||||||
const dbPermission = await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Permission Deleted!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with not found (404) if permission with provided id does not exists', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.delete('/SASjsApi/permission/123')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Permission not found.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('get', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
path: '/test-1',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
path: '/test-2',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should give a list of all permissions when user is admin', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/permission/')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`should give a list of user's own permissions when user is not admin`, async () => {
|
|
||||||
const nonAdminUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'get' + user.username
|
|
||||||
})
|
|
||||||
const accessToken = await generateAndSaveToken(nonAdminUser.id)
|
|
||||||
await permissionController.createPermission({
|
|
||||||
path: '/test-1',
|
|
||||||
type: PermissionType.route,
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: nonAdminUser.id,
|
|
||||||
setting: PermissionSettingForRoute.grant
|
|
||||||
})
|
|
||||||
|
|
||||||
const permissionCount = 1
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/permission/')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(permissionCount)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('verify', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
path: '/SASjsApi/drive/deploy',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest
|
|
||||||
.spyOn(DriveController.prototype, 'deploy')
|
|
||||||
.mockImplementation((deployPayload) =>
|
|
||||||
Promise.resolve({
|
|
||||||
status: 'success',
|
|
||||||
message: 'Files deployed successfully to @sasjs/server.'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create files in SASJS drive', async () => {
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get('/SASjsApi/drive/deploy')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send(deployPayload)
|
|
||||||
.expect(200)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond unauthorized', async () => {
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get('/SASjsApi/drive/deploy/upload')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const generateSaveTokenAndCreateUser = async (
|
|
||||||
someUser?: any
|
|
||||||
): Promise<string> => {
|
|
||||||
const dbUser = await userController.createUser(someUser ?? adminUser)
|
|
||||||
|
|
||||||
return generateAndSaveToken(dbUser.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
|
||||||
const adminAccessToken = generateAccessToken({
|
|
||||||
clientId,
|
|
||||||
userId
|
|
||||||
})
|
|
||||||
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
|
||||||
return adminAccessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteAllPermissions = async () => {
|
|
||||||
const { collections } = mongoose.connection
|
|
||||||
const collection = collections['permissions']
|
|
||||||
await collection.deleteMany({})
|
|
||||||
}
|
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
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,
|
|
||||||
PermissionController,
|
|
||||||
PermissionType,
|
|
||||||
PermissionSettingForRoute,
|
|
||||||
PrincipalType
|
|
||||||
} from '../../../controllers/'
|
|
||||||
import {
|
|
||||||
generateAccessToken,
|
|
||||||
saveTokensInDB,
|
|
||||||
getFilesFolder,
|
|
||||||
RunTimeType,
|
|
||||||
generateUniqueFileName,
|
|
||||||
getSessionsFolder
|
|
||||||
} from '../../../utils'
|
|
||||||
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
|
||||||
import {
|
|
||||||
SessionController,
|
|
||||||
SASSessionController
|
|
||||||
} from '../../../controllers/internal'
|
|
||||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
|
||||||
import { Session, SessionState } from '../../../types'
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
displayName: 'Test User',
|
|
||||||
username: 'testUsername',
|
|
||||||
password: '87654321',
|
|
||||||
isAdmin: false,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleSasProgram = '%put hello world!;'
|
|
||||||
const sampleJsProgram = `console.log('hello world!/')`
|
|
||||||
const samplePyProgram = `print('hello world!/')`
|
|
||||||
|
|
||||||
const filesFolder = getFilesFolder()
|
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
|
||||||
|
|
||||||
let app: Express
|
|
||||||
let accessToken: string
|
|
||||||
|
|
||||||
describe('stp', () => {
|
|
||||||
let con: Mongoose
|
|
||||||
let mongoServer: MongoMemoryServer
|
|
||||||
const userController = new UserController()
|
|
||||||
const permissionController = new PermissionController()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await appPromise
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
|
||||||
const dbUser = await userController.createUser(user)
|
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
await permissionController.createPermission({
|
|
||||||
path: '/SASjsApi/stp/execute',
|
|
||||||
type: PermissionType.route,
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSettingForRoute.grant
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await con.connection.dropDatabase()
|
|
||||||
await con.connection.close()
|
|
||||||
await mongoServer.stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
describe('get', () => {
|
|
||||||
describe('with runtime js', () => {
|
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.JS]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.JS, RunTimeType.SAS],
|
|
||||||
200,
|
|
||||||
RunTimeType.JS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when js program is not present but sas program exists', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime py', () => {
|
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.PY]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute python program when python, js and sas programs are present', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
|
||||||
200,
|
|
||||||
RunTimeType.PY
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when py program is not present but js or sas program exists', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime sas', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.SAS]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs are present', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when sas program do not exit but js exists', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime js and sas', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.JS, RunTimeType.SAS]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.SAS, RunTimeType.JS],
|
|
||||||
200,
|
|
||||||
RunTimeType.JS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when js program is not present but sas program exists', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime py and sas', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.PY, RunTimeType.SAS]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute python program when both python and sas program are present', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.PY, RunTimeType.SAS],
|
|
||||||
200,
|
|
||||||
RunTimeType.PY
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when python program is not present but sas program exists', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime sas and js', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.SAS, RunTimeType.JS]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs exist', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.SAS, RunTimeType.JS],
|
|
||||||
200,
|
|
||||||
RunTimeType.SAS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute js program when sas program is not present but js program exists', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime sas and py', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.SAS, RunTimeType.PY]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when both sas and python programs exist', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.SAS, RunTimeType.PY],
|
|
||||||
200,
|
|
||||||
RunTimeType.SAS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute python program when sas program is not present but python program exists', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when both sas and python programs do not exist', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime sas, js and py', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.SAS, RunTimeType.JS, RunTimeType.PY]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when it exists, no matter js and python programs exist or not', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.SAS, RunTimeType.PY, RunTimeType.JS],
|
|
||||||
200,
|
|
||||||
RunTimeType.SAS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute js program when sas program is absent but js and python programs are present', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.JS, RunTimeType.PY],
|
|
||||||
200,
|
|
||||||
RunTimeType.JS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute python program when both sas and js programs are not present', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when no program exists', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime js, sas and py', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute js program when it exists, no matter sas and python programs exist or not', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY],
|
|
||||||
200,
|
|
||||||
RunTimeType.JS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when js program is absent but sas and python programs are present', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.SAS, RunTimeType.PY],
|
|
||||||
200,
|
|
||||||
RunTimeType.SAS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute python program when both sas and js programs are not present', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when no program exists', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with runtime py, sas and js', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
process.runTimes = [RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS]
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules() // it clears the cache
|
|
||||||
setupMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute python program when it exists, no matter sas and js programs exist or not', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
|
||||||
200,
|
|
||||||
RunTimeType.PY
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute sas program when python program is absent but sas and js programs are present', async () => {
|
|
||||||
await makeRequestAndAssert(
|
|
||||||
[RunTimeType.SAS, RunTimeType.JS],
|
|
||||||
200,
|
|
||||||
RunTimeType.SAS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute js program when both sas and python programs are not present', async () => {
|
|
||||||
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when no program exists', async () => {
|
|
||||||
await makeRequestAndAssert([], 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeRequestAndAssert = async (
|
|
||||||
programTypes: RunTimeType[],
|
|
||||||
expectedStatusCode: number,
|
|
||||||
expectedRuntime?: RunTimeType
|
|
||||||
) => {
|
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
|
||||||
for (const programType of programTypes) {
|
|
||||||
if (programType === RunTimeType.JS)
|
|
||||||
await createFile(
|
|
||||||
path.join(filesFolder, `${programPath}.js`),
|
|
||||||
sampleJsProgram
|
|
||||||
)
|
|
||||||
else if (programType === RunTimeType.PY)
|
|
||||||
await createFile(
|
|
||||||
path.join(filesFolder, `${programPath}.py`),
|
|
||||||
samplePyProgram
|
|
||||||
)
|
|
||||||
else if (programType === RunTimeType.SAS)
|
|
||||||
await createFile(
|
|
||||||
path.join(filesFolder, `${programPath}.sas`),
|
|
||||||
sampleSasProgram
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(expectedStatusCode)
|
|
||||||
|
|
||||||
if (expectedRuntime)
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expectedRuntime,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
|
||||||
const accessToken = generateAccessToken({
|
|
||||||
clientId,
|
|
||||||
userId
|
|
||||||
})
|
|
||||||
await saveTokensInDB(userId, clientId, accessToken, 'refreshToken')
|
|
||||||
return accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupMocks = async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(SASSessionController.prototype, 'getSession')
|
|
||||||
.mockImplementation(mockedGetSession)
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(SASSessionController.prototype, 'getSession')
|
|
||||||
.mockImplementation(mockedGetSession)
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(ProcessProgramModule, 'processProgram')
|
|
||||||
.mockImplementation(() => Promise.resolve())
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockedGetSession = async () => {
|
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
|
||||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
|
||||||
// death time of session is 15 mins from creation
|
|
||||||
const deathTimeStamp = (
|
|
||||||
parseInt(creationTimeStamp) +
|
|
||||||
15 * 60 * 1000 -
|
|
||||||
1000
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
const session: Session = {
|
|
||||||
id: sessionId,
|
|
||||||
state: SessionState.pending,
|
|
||||||
creationTimeStamp,
|
|
||||||
deathTimeStamp,
|
|
||||||
path: sessionFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
@@ -3,29 +3,23 @@ import mongoose, { Mongoose } from 'mongoose'
|
|||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, GroupController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import {
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
generateAccessToken,
|
|
||||||
saveTokensInDB,
|
|
||||||
AuthProviderType
|
|
||||||
} from '../../../utils'
|
|
||||||
import User from '../../../model/User'
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -70,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 () => {
|
||||||
@@ -115,16 +94,16 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Conflict if username is already present', async () => {
|
it('should respond with Forbidden if username is already present', async () => {
|
||||||
await controller.createUser(user)
|
await controller.createUser(user)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/user')
|
.post('/SASjsApi/user')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send(user)
|
.send(user)
|
||||||
.expect(409)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Username already exists.')
|
expect(res.text).toEqual('Error: Username already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -231,36 +210,6 @@ describe('user', () => {
|
|||||||
.expect(400)
|
.expect(400)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Method Not Allowed, when updating username of user created by an external auth provider', async () => {
|
|
||||||
const dbUser = await User.create({
|
|
||||||
...user,
|
|
||||||
authProvider: AuthProviderType.LDAP
|
|
||||||
})
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser!.id)
|
|
||||||
const newUsername = 'newUsername'
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.patch(`/SASjsApi/user/${dbUser!.id}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ username: newUsername })
|
|
||||||
.expect(405)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Method Not Allowed, when updating displayName of user created by an external auth provider', async () => {
|
|
||||||
const dbUser = await User.create({
|
|
||||||
...user,
|
|
||||||
authProvider: AuthProviderType.LDAP
|
|
||||||
})
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser!.id)
|
|
||||||
const newDisplayName = 'My new display Name'
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.patch(`/SASjsApi/user/${dbUser!.id}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ displayName: newDisplayName })
|
|
||||||
.expect(405)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/user/1234')
|
.patch('/SASjsApi/user/1234')
|
||||||
@@ -289,118 +238,22 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Conflict if username is already present', async () => {
|
it('should respond with Forbidden if username is already present', async () => {
|
||||||
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)
|
||||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({ username: dbUser2.username })
|
.send({ username: dbUser2.username })
|
||||||
.expect(409)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Username already exists.')
|
expect(res.text).toEqual('Error: Username already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('by username', () => {
|
|
||||||
it('should respond with updated user when admin user requests', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const newDisplayName = 'My new display Name'
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ ...user, displayName: newDisplayName })
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.username).toEqual(user.username)
|
|
||||||
expect(res.body.displayName).toEqual(newDisplayName)
|
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with updated user when user himself requests', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
const newDisplayName = 'My new display Name'
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
displayName: newDisplayName,
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
})
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.username).toEqual(user.username)
|
|
||||||
expect(res.body.displayName).toEqual(newDisplayName)
|
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
const newDisplayName = 'My new display Name'
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ ...user, displayName: newDisplayName })
|
|
||||||
.expect(400)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch('/SASjsApi/user/by/username/1234')
|
|
||||||
.send(user)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
|
|
||||||
const dbUser1 = await controller.createUser(user)
|
|
||||||
const dbUser2 = await controller.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'randomUser'
|
|
||||||
})
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send(user)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Conflict if username is already present', async () => {
|
|
||||||
const dbUser1 = await controller.createUser(user)
|
|
||||||
const dbUser2 = await controller.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'randomuser'
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ username: dbUser2.username })
|
|
||||||
.expect(409)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Username already exists.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
@@ -481,7 +334,7 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
|
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
@@ -489,94 +342,11 @@ describe('user', () => {
|
|||||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send({ password: 'incorrectpassword' })
|
.send({ password: 'incorrectpassword' })
|
||||||
.expect(401)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Invalid password.')
|
expect(res.text).toEqual('Error: Invalid password.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('by username', () => {
|
|
||||||
it('should respond with OK when admin user requests', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with OK when user himself requests', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ password: user.password })
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request when user himself requests and password is missing', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"password" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized when access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.delete('/SASjsApi/user/by/username/RandomUsername')
|
|
||||||
.send(user)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
|
|
||||||
const dbUser1 = await controller.createUser(user)
|
|
||||||
const dbUser2 = await controller.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'randomUser'
|
|
||||||
})
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send(user)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send({ password: 'incorrectpassword' })
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Invalid password.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
@@ -590,26 +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)
|
|
||||||
expect(res.body.groups).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -623,8 +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)
|
|
||||||
expect(res.body.groups).toEqual([])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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 () => {
|
||||||
@@ -646,35 +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()
|
|
||||||
expect(res.body.groups).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with user along with associated groups', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const userId = dbUser.id
|
|
||||||
const accessToken = await generateAndSaveToken(userId)
|
|
||||||
|
|
||||||
const group = {
|
|
||||||
name: 'DCGroup1',
|
|
||||||
description: 'DC group for testing purposes.'
|
|
||||||
}
|
|
||||||
const groupController = new GroupController()
|
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
|
||||||
|
|
||||||
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)
|
|
||||||
expect(res.body.groups.length).toBeGreaterThan(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
@@ -687,98 +407,18 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if userId is incorrect', async () => {
|
it('should respond with Forbidden if userId is incorrect', async () => {
|
||||||
await controller.createUser(user)
|
await controller.createUser(user)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/user/1234')
|
.get('/SASjsApi/user/1234')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('User is not found.')
|
expect(res.text).toEqual('Error: User is not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('by username', () => {
|
|
||||||
it('should respond with user autoExec when same user requests', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const userId = dbUser.id
|
|
||||||
const accessToken = await generateAndSaveToken(userId)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
|
||||||
.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 res = await request(app)
|
|
||||||
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
|
||||||
.auth(adminAccessToken, { 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 when access token is not of an admin account', async () => {
|
|
||||||
const accessToken = await generateSaveTokenAndCreateUser({
|
|
||||||
...user,
|
|
||||||
username: 'randomUser'
|
|
||||||
})
|
|
||||||
|
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
|
||||||
.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).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/user/by/username/randomUsername')
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found if username is incorrect', async () => {
|
|
||||||
await controller.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/user/by/username/randomUsername')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('User is not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getAll', () => {
|
describe('getAll', () => {
|
||||||
@@ -805,14 +445,12 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName,
|
displayName: adminUser.displayName
|
||||||
isAdmin: adminUser.isAdmin
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName
|
||||||
isAdmin: user.isAdmin
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -833,14 +471,12 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName,
|
displayName: adminUser.displayName
|
||||||
isAdmin: adminUser.isAdmin
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: 'randomUser',
|
username: 'randomUser',
|
||||||
displayName: user.displayName,
|
displayName: user.displayName
|
||||||
isAdmin: user.isAdmin
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,289 +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 () => {
|
|
||||||
const res = await request(app).get('/').expect(200)
|
|
||||||
|
|
||||||
expect(res.text).toMatch(
|
|
||||||
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('SASLogon/authorize', () => {
|
|
||||||
let csrfToken: string
|
|
||||||
let authCookies: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
;({ csrfToken } = await getCSRF(app))
|
|
||||||
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const credentials = {
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
}
|
|
||||||
|
|
||||||
;({ authCookies } = await performLogin(app, credentials, 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].join('; '))
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send({ clientId })
|
|
||||||
|
|
||||||
expect(res.body).toHaveProperty('code')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if CSRF Token is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASLogon/authorize')
|
|
||||||
.set('Cookie', [authCookies].join('; '))
|
|
||||||
.send({ clientId })
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Invalid CSRF token!')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if clientId is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASLogon/authorize')
|
|
||||||
.set('Cookie', [authCookies].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].join('; '))
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send({
|
|
||||||
clientId: 'WrongClientID'
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Invalid clientId.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('SASLogon/login', () => {
|
|
||||||
let csrfToken: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
;({ csrfToken } = 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('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,
|
|
||||||
isAdmin: user.isAdmin,
|
|
||||||
needsToUpdatePassword: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with too many requests when attempting with invalid password for a same user too many times', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const promises: request.Test[] = []
|
|
||||||
|
|
||||||
const maxConsecutiveFailsByUsernameAndIp = Number(
|
|
||||||
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
|
||||||
)
|
|
||||||
|
|
||||||
Array(maxConsecutiveFailsByUsernameAndIp + 1)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => {
|
|
||||||
promises.push(
|
|
||||||
request(app)
|
|
||||||
.post('/SASLogon/login')
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: 'invalid-password'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASLogon/login')
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
})
|
|
||||||
.expect(429)
|
|
||||||
|
|
||||||
expect(res.text).toContain('Too Many Requests!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with too many requests when attempting with invalid credentials for different users but with same ip too many times', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const promises: request.Test[] = []
|
|
||||||
|
|
||||||
const maxWrongAttemptsByIpPerDay = Number(
|
|
||||||
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
|
|
||||||
)
|
|
||||||
|
|
||||||
Array(maxWrongAttemptsByIpPerDay + 1)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => {
|
|
||||||
promises.push(
|
|
||||||
request(app)
|
|
||||||
.post('/SASLogon/login')
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send({
|
|
||||||
username: `user${i}`,
|
|
||||||
password: 'invalid-password'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASLogon/login')
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
})
|
|
||||||
.expect(429)
|
|
||||||
|
|
||||||
expect(res.text).toContain('Too Many Requests!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if CSRF Token is not present', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASLogon/login')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Invalid CSRF token!')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if CSRF Token is invalid', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASLogon/login')
|
|
||||||
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Invalid CSRF token!')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const getCSRF = async (app: Express) => {
|
|
||||||
// make request to get CSRF
|
|
||||||
const { text } = await request(app).get('/')
|
|
||||||
|
|
||||||
return { csrfToken: extractCSRF(text) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const performLogin = async (
|
|
||||||
app: Express,
|
|
||||||
credentials: { username: string; password: string },
|
|
||||||
csrfToken: string
|
|
||||||
) => {
|
|
||||||
const { header } = await request(app)
|
|
||||||
.post('/SASLogon/login')
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send(credentials)
|
|
||||||
|
|
||||||
return {
|
|
||||||
authCookies:
|
|
||||||
(header['set-cookie'] as unknown as string[] | undefined)?.join() || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractCSRF = (text: string) =>
|
|
||||||
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
|
||||||
text
|
|
||||||
)![1]
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import {
|
import { executeProgramRawValidation } from '../../utils'
|
||||||
executeProgramRawValidation,
|
|
||||||
triggerProgramValidation
|
|
||||||
} from '../../utils'
|
|
||||||
import { STPController } from '../../controllers/'
|
import { STPController } from '../../controllers/'
|
||||||
import { FileUploadController } from '../../controllers/internal'
|
import { FileUploadController } from '../../controllers/internal'
|
||||||
|
|
||||||
@@ -16,11 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeGetRequest(
|
const response = await controller.executeReturnRaw(req, query._program)
|
||||||
req,
|
|
||||||
query._program,
|
|
||||||
query._debug
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response instanceof Buffer) {
|
if (response instanceof Buffer) {
|
||||||
res.writeHead(200, (req as any).sasHeaders)
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
@@ -41,25 +34,23 @@ stpRouter.post(
|
|||||||
'/execute',
|
'/execute',
|
||||||
fileUploadController.preUploadMiddleware,
|
fileUploadController.preUploadMiddleware,
|
||||||
fileUploadController.getMulterUploadObject().any(),
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
async (req, res: any) => {
|
async (req: any, res: any) => {
|
||||||
// below validations are moved to preUploadMiddleware
|
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)
|
|
||||||
|
|
||||||
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executePostRequest(
|
const response = await controller.executeReturnJson(
|
||||||
req,
|
req,
|
||||||
req.body,
|
body,
|
||||||
req.query?._program as string
|
query?._program
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: investigate if this code is required
|
if (response instanceof Buffer) {
|
||||||
// if (response instanceof Buffer) {
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
// res.writeHead(200, (req as any).sasHeaders)
|
return res.end(response)
|
||||||
// return res.end(response)
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -72,28 +63,4 @@ stpRouter.post(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
stpRouter.post('/trigger', async (req, res) => {
|
|
||||||
const { error, value: query } = triggerProgramValidation(req.query)
|
|
||||||
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.triggerProgram(
|
|
||||||
req,
|
|
||||||
query._program,
|
|
||||||
query._debug,
|
|
||||||
query.expiresAfterMins
|
|
||||||
)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default stpRouter
|
export default stpRouter
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
import {
|
import {
|
||||||
deleteUserValidation,
|
deleteUserValidation,
|
||||||
getUserValidation,
|
|
||||||
registerUserValidation,
|
registerUserValidation,
|
||||||
updateUserValidation
|
updateUserValidation
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -23,7 +22,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
|
|||||||
const response = await controller.createUser(body)
|
const response = await controller.createUser(body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -33,115 +32,40 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
const response = await controller.getAllUsers()
|
const response = await controller.getAllUsers()
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
userRouter.get(
|
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
|
||||||
'/by/username/:username',
|
|
||||||
authenticateAccessToken,
|
|
||||||
async (req, res) => {
|
|
||||||
const { error, value: params } = getUserValidation(req.params)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
const { username } = params
|
|
||||||
|
|
||||||
const controller = new UserController()
|
|
||||||
try {
|
|
||||||
const response = await controller.getUserByUsername(req, username)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(err.code).send(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
userRouter.get('/:userId', authenticateAccessToken, async (req, 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(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
userRouter.patch(
|
|
||||||
'/by/username/:username',
|
|
||||||
authenticateAccessToken,
|
|
||||||
verifyAdminIfNeeded,
|
|
||||||
async (req, res) => {
|
|
||||||
const { user } = req
|
|
||||||
const { error: errorUsername, value: params } = getUserValidation(
|
|
||||||
req.params
|
|
||||||
)
|
|
||||||
if (errorUsername)
|
|
||||||
return res.status(400).send(errorUsername.details[0].message)
|
|
||||||
|
|
||||||
const { username } = params
|
|
||||||
|
|
||||||
// only an admin can update `isActive` and `isAdmin` fields
|
|
||||||
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
const controller = new UserController()
|
|
||||||
try {
|
|
||||||
const response = await controller.updateUserByUsername(username, body)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(err.code).send(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
userRouter.patch(
|
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(err.code).send(err.message)
|
res.status(403).send(err.toString())
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
userRouter.delete(
|
|
||||||
'/by/username/:username',
|
|
||||||
authenticateAccessToken,
|
|
||||||
verifyAdminIfNeeded,
|
|
||||||
async (req, res) => {
|
|
||||||
const { user } = req
|
|
||||||
const { error: errorUsername, value: params } = getUserValidation(
|
|
||||||
req.params
|
|
||||||
)
|
|
||||||
if (errorUsername)
|
|
||||||
return res.status(400).send(errorUsername.details[0].message)
|
|
||||||
|
|
||||||
const { username } = params
|
|
||||||
|
|
||||||
// only an admin can delete user without providing password
|
|
||||||
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
const controller = new UserController()
|
|
||||||
try {
|
|
||||||
await controller.deleteUserByUsername(username, data, user!.isAdmin)
|
|
||||||
res.status(200).send('Account Deleted!')
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(err.code).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -150,20 +74,20 @@ 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(err.code).send(err.message)
|
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'
|
||||||
@@ -23,21 +24,13 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
|||||||
${style}
|
${style}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<h1>App Stream</h1>
|
||||||
<a href="/"><img src="/logo.png" alt="logo" class="logo"></a>
|
|
||||||
<h1>App Stream</h1>
|
|
||||||
</header>
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
${Object.entries(appStreamConfig)
|
${Object.entries(appStreamConfig)
|
||||||
.map(([streamServiceName, entry]) =>
|
.map(([streamServiceName, entry]) =>
|
||||||
singleAppStreamHtml(
|
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
||||||
streamServiceName,
|
)
|
||||||
entry.appLoc,
|
.join('')}
|
||||||
entry.streamLogo
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
|
|
||||||
<a class="app" title="Upload build.json">
|
<a class="app" title="Upload build.json">
|
||||||
<input id="fileId" type="file" hidden />
|
<input id="fileId" type="file" hidden />
|
||||||
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
|
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
|
||||||
@@ -46,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>`
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { Request } from 'express'
|
import express from 'express'
|
||||||
import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
|
|
||||||
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 appStreams: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/', authenticateAccessToken, async (req, res) => {
|
router.get('/', async (_, res) => {
|
||||||
const content = appStreamHtml(process.appStreamConfig)
|
const content = appStreamHtml(process.appStreamConfig)
|
||||||
|
|
||||||
res.cookie('XSRF-TOKEN', generateCSRFToken())
|
|
||||||
|
|
||||||
return res.send(content)
|
return res.send(content)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -25,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)
|
||||||
@@ -47,7 +42,7 @@ export const publishAppStream = async (
|
|||||||
streamServiceName = `AppStreamName${appCount + 1}`
|
streamServiceName = `AppStreamName${appCount + 1}`
|
||||||
}
|
}
|
||||||
|
|
||||||
appStreams[streamServiceName] = pathToDeployment
|
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
||||||
|
|
||||||
addEntryToAppStreamConfig(
|
addEntryToAppStreamConfig(
|
||||||
streamServiceName,
|
streamServiceName,
|
||||||
@@ -58,7 +53,7 @@ export const publishAppStream = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const sasJsPort = process.env.PORT || 5000
|
const sasJsPort = process.env.PORT || 5000
|
||||||
process.logger.info(
|
console.log(
|
||||||
'Serving Stream App: ',
|
'Serving Stream App: ',
|
||||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||||
)
|
)
|
||||||
@@ -67,26 +62,4 @@ export const publishAppStream = async (
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get(`/*`, authenticateAccessToken, function (req: Request, res, next) {
|
|
||||||
const reqPath = req.path.replace(/^\//, '')
|
|
||||||
|
|
||||||
// Redirecting to url with trailing slash for appStream base URL only
|
|
||||||
if (reqPath.split('/').length === 1 && !reqPath.endsWith('/'))
|
|
||||||
// navigating to same url with slash at start
|
|
||||||
return res.redirect(301, `${reqPath}/`)
|
|
||||||
|
|
||||||
const appStream = reqPath.split('/')[0]
|
|
||||||
const appStreamFilesPath = appStreams[appStream]
|
|
||||||
if (appStreamFilesPath) {
|
|
||||||
// resourcePath is without appStream base path
|
|
||||||
const resourcePath = reqPath.split('/').slice(1).join('/') || 'index.html'
|
|
||||||
|
|
||||||
req.url = resourcePath
|
|
||||||
|
|
||||||
return express.static(appStreamFilesPath)(req, res, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send("There's no App Stream available here.")
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
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>`
|
||||||
@@ -5,72 +5,18 @@ export const style = `<style>
|
|||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 50px;
|
|
||||||
}
|
}
|
||||||
.app-container .app {
|
.app-container .app {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 180px;
|
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
text-decoration: none;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
background: #efefef;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 7px;
|
|
||||||
border: 1px solid #d7d7d7;
|
|
||||||
}
|
}
|
||||||
.app-container .app img{
|
.app-container .app img{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 30px);
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
#uploadButton {
|
|
||||||
border: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
#uploadButton:focus {
|
|
||||||
outline: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
#uploadMessage {
|
|
||||||
position: relative;
|
|
||||||
bottom: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
|
||||||
box-shadow: rgb(0 0 0 / 20%) 0px 2px 4px -1px, rgb(0 0 0 / 14%) 0px 4px 5px 0px, rgb(0 0 0 / 12%) 0px 1px 10px 0px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 0px;
|
|
||||||
left: auto;
|
|
||||||
right: 0px;
|
|
||||||
background-color: rgb(0, 0, 0);
|
|
||||||
color: rgb(255, 255, 255);
|
|
||||||
z-index: 1201;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
margin: 13px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header a {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .logo {
|
|
||||||
width: 35px;
|
|
||||||
margin-left: 10px;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
</style>`
|
</style>`
|
||||||
|
|||||||
@@ -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 '../middlewares'
|
|
||||||
|
|
||||||
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('/', webRouter)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,8 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import sas9WebRouter from './sas9-web'
|
|
||||||
import sasViyaWebRouter from './sasviya-web'
|
|
||||||
import webRouter from './web'
|
import webRouter from './web'
|
||||||
import { MOCK_SERVERTYPEType } from '../../utils'
|
|
||||||
import { csrfProtection } from '../../middlewares'
|
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
const { MOCK_SERVERTYPE } = process.env
|
router.use('/', webRouter)
|
||||||
|
|
||||||
switch (MOCK_SERVERTYPE) {
|
|
||||||
case MOCK_SERVERTYPEType.SAS9: {
|
|
||||||
router.use('/', sas9WebRouter)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case MOCK_SERVERTYPEType.SASVIYA: {
|
|
||||||
router.use('/', sasViyaWebRouter)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
router.use('/', csrfProtection, webRouter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { generateCSRFToken } from '../../middlewares'
|
|
||||||
import { WebController } from '../../controllers'
|
|
||||||
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
|
||||||
import multer from 'multer'
|
|
||||||
import path from 'path'
|
|
||||||
import dotenv from 'dotenv'
|
|
||||||
import { FileUploadController } from '../../controllers/internal'
|
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
const sas9WebRouter = express.Router()
|
|
||||||
const webController = new WebController()
|
|
||||||
// Mock controller must be singleton because it keeps the states
|
|
||||||
// for example `isLoggedIn` and potentially more in future mocks
|
|
||||||
const controller = new MockSas9Controller()
|
|
||||||
const fileUploadController = new FileUploadController()
|
|
||||||
|
|
||||||
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
|
|
||||||
})
|
|
||||||
|
|
||||||
sas9WebRouter.get('/', async (req, res) => {
|
|
||||||
let response
|
|
||||||
try {
|
|
||||||
response = await webController.home()
|
|
||||||
} catch (_) {
|
|
||||||
response = '<html><head></head><body>Web Build is not present</body></html>'
|
|
||||||
} finally {
|
|
||||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
|
||||||
const injectedContent = response?.replace(
|
|
||||||
'</head>',
|
|
||||||
`${codeToInject}</head>`
|
|
||||||
)
|
|
||||||
|
|
||||||
return res.send(injectedContent)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
|
||||||
const response = await controller.sasStoredProcess(req)
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
res.redirect(response.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
res.send(response.content)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
|
|
||||||
const response = await controller.sasStoredProcessDoGet(req)
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
res.redirect(response.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
res.send(response.content)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sas9WebRouter.post(
|
|
||||||
'/SASStoredProcess/do/',
|
|
||||||
fileUploadController.preUploadMiddleware,
|
|
||||||
fileUploadController.getMulterUploadObject().any(),
|
|
||||||
async (req, res) => {
|
|
||||||
const response = await controller.sasStoredProcessDoPost(req)
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
res.redirect(response.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
res.send(response.content)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
|
||||||
const response = await controller.loginGet()
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
res.redirect(response.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
res.send(response.content)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
|
|
||||||
const response = await controller.loginPost(req)
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
res.redirect(response.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
res.send(response.content)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sas9WebRouter.get('/SASLogon/logout', async (req, res) => {
|
|
||||||
const response = await controller.logout(req)
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
res.redirect(response.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
res.send(response.content)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sas9WebRouter.get('/SASStoredProcess/Logoff', async (req, res) => {
|
|
||||||
const response = await controller.logoff(req)
|
|
||||||
|
|
||||||
if (response.redirect) {
|
|
||||||
res.redirect(response.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
res.send(response.content)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default sas9WebRouter
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user