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

Compare commits

..

5 Commits

Author SHA1 Message Date
Saad Jutt
c9e11a5646 chore: removed public htmls 2021-10-14 18:54:11 +05:00
Saad Jutt
4912915a6e fix: read security code from file + redirect 2021-10-14 18:51:32 +05:00
Saad Jutt
8fb7129f86 fix: using passport for azure and local authentication 2021-10-14 13:50:10 +05:00
Saad Jutt
3806b4bbb1 chore: code refactored 2021-10-13 21:47:23 +05:00
Saad Jutt
bbd7133f5c feat: authentication with azure all routes are protected 2021-10-12 21:48:35 +05:00
239 changed files with 7487 additions and 74652 deletions

View File

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

View File

@@ -1,32 +0,0 @@
**/.classpath
**/.dockerignore
**/.env
!.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
api/build
api/coverage
api/build
api/node_modules
api/public
api/web
web/build
web/node_modules
README.md

View File

@@ -1,7 +0,0 @@
SAS_EXEC_PATH=<path to folder containing SAS executable>
SAS_EXEC_NAME=<name of SAS executable file>
PORT_API=<port for sasjs server (api)>
PORT_WEB=<port for sasjs web component(react)>
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>

View File

@@ -1,114 +0,0 @@
# CONTRIBUTING
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
The app can be deployed using Docker or NodeJS.
## Configuration
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
* `.env` - the root .env file is used only for Docker deploys.
* `api/.env` - this is the primary file used in NodeJS deploys
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
## Using Docker
### Docker Development Mode
Command to run docker for development:
```
docker-compose up -d
```
It uses default docker compose file i.e. `docker-compose.yml` present at root.
It will build following images if running first time:
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
### Docker Production Mode
Command to run docker for production:
```
docker-compose -f docker-compose.prod.yml up -d
```
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
It will build following images if running first time:
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
## Using NodeJS:
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
### NodeJS Development Mode
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
### NodeJS Dev - Single Port
Here the environment variables should be configured under `api.env`. Then:
```
cd ./web && npm i && npm build
cd ../api && npm i && npm start
```
### NodeJS Dev - Seperate Ports
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
#### API server
```
cd api
npm install
npm start
```
#### Web Server
```
cd web
npm install
npm start
```
#### NodeJS Production Mode
Update the `.env` file in the *api* folder. Then:
```
npm run server
```
This will install/build `web` and install `api`, then start prod server.
## Executables
In order to generate the final executables:
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm run exe
```
This will install/build web app and install/create executables of sasjs server at root `./executables`
## Releases
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: [sasjs]

View File

@@ -1,95 +1,30 @@
name: SASjs Server Build
on:
push:
pull_request:
jobs:
lint:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm ci
- name: Check Api Code Style
run: npm run lint-api
- name: Check Web Code Style
run: npm run lint-web
build-api:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
steps:
- 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
working-directory: ./api
run: npm ci
- name: Check Code Style
run: npm run lint
- name: Run Unit Tests
working-directory: ./api
run: npm test
env:
CI: true
MODE: 'server'
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_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
working-directory: ./api
run: npm run build
env:
CI: true
build-web:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
steps:
- 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
working-directory: ./web
run: npm ci
# TODO: Uncomment next step when unit tests provided
# - name: Run Unit Tests
# working-directory: ./web
# run: npm test
- name: Build Package
working-directory: ./web
run: npm run build
run: npm run package:lib
env:
CI: true

View File

@@ -1,59 +0,0 @@
name: SASjs Server Executable Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
steps:
- name: Checkout
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
working-directory: ./web
run: npm ci
- name: Build WEB
working-directory: ./web
run: npm run build
env:
CI: true
- name: Install Dependencies API
working-directory: ./api
run: npm ci
- name: Build Executables
working-directory: ./api
run: npm run exe
env:
CI: true
- name: Compress Executables
working-directory: ./executables
run: |
zip linux.zip api-linux
zip macos.zip api-macos
zip windows.zip api-win.exe
- name: Install Semantic Release and plugins
run: |
npm i
npm i -g semantic-release
- name: Release
run: |
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release

7
.gitignore vendored
View File

@@ -4,12 +4,7 @@ node_modules/
.DS_Store
.env*
sas/
sasjs_root/
tmp/
build/
sasjsbuild/
sasjscore/
certificates/
executables/
.env
api/csp.config.json
security.json

View File

@@ -1,10 +0,0 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others.
tasks:
- init: npm install
vscode:
extensions:
- dbaeumer.vscode-eslint
- sasjs.sasjs-for-vscode

1
.nvmrc
View File

@@ -1 +0,0 @@
v16.15.1

View File

@@ -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'"
}
]
]
}

11
.vscode/launch.json vendored
View File

@@ -1,11 +0,0 @@
{
"configurations": [
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node"
}
]
}

View File

@@ -1,5 +0,0 @@
{
"cSpell.words": [
"autoexec"
]
}

35
.vscode/tasks.json vendored
View File

@@ -1,35 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "docker-build",
"label": "docker-build",
"platform": "node",
"dockerBuild": {
"dockerfile": "${workspaceFolder}/Dockerfile",
"context": "${workspaceFolder}",
"pull": true
}
},
{
"type": "docker-run",
"label": "docker-run: release",
"dependsOn": ["docker-build"],
"platform": "node"
},
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": ["docker-build"],
"dockerRun": {
"env": {
"DEBUG": "*",
"NODE_ENV": "development"
}
},
"node": {
"enableDebugging": true
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
FROM node:lts-alpine
WORKDIR /usr/server/api
COPY ["package.json","package-lock.json", "./"]
RUN npm ci
COPY ./api .
COPY ./certificates ../certificates
# RUN chown -R node /usr/server/api
# USER node
CMD ["npm","start"]

View File

@@ -1,11 +0,0 @@
FROM node:lts-alpine
RUN npm install -g @sasjs/cli
WORKDIR /usr/server/
COPY . .
RUN cd web && npm ci --silent
RUN cd web && REACT_APP_CLIENT_ID=clientID1 npm run build
RUN cd api && npm ci --silent
# RUN chown -R node /usr/server/api
# USER node
WORKDIR /usr/server/api
CMD ["npm","run","start:prod"]

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 SASjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

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

232
README.md
View File

@@ -1,230 +1,16 @@
# SASjs Server
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or it could even run locally on your desktop. It provides the following functionality:
[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-)
* Virtual filesystem for storing SAS programs and other content
* Ability to execute Stored Programs from a URL
* Ability to create web apps using simple Desktop SAS
<!-- ALL-CONTRIBUTORS-BADGE:END -->
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
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:
## Configuration
- Virtual filesystem for storing SAS programs and other content
- Ability to execute Stored Programs from a URL
- Ability to create web apps using simple Desktop SAS
- REST API with Swagger Docs
Configuration is made in the `configuration` section of `package.json`:
One major benefit of using SASjs Server alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library, is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentication, and a database)
## Installation
Installation can be made programmatically using command line, or by manually downloading and running the executable.
### Programmatic
Fetch the relevant package from github using `curl`, eg as follows (for linux):
```bash
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
```
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
### Manual
1. Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
2. Trigger by double clicking (windows) or executing from commandline.
You are presented with two prompts (if not set as ENV vars):
- Location of your `sas.exe` / `sas.sh` executable
- Path to a filesystem location for Stored Programs and temporary files
## ENV Var configuration
When launching the app, it will make use of specific environment variables. These can be set in the following places:
- Configured globally in `/etc/environment` file
- Export in terminal or shell script (`export VAR=VALUE`)
- Prepended in the command
- Enter in the `.env` file alongside the executable
Example contents of a `.env` file:
```
#
## 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=
# 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 working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root
# options: [http|https] default: http
PROTOCOL=
# default: 5000
PORT=
#
## 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: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
# options: <http://localhost:3000 https://abc.com ...> space separated urls
WHITELIST=
# HELMET Cross Origin Embedder Policy
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# options: [true|false] default: true
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
HELMET_COEP=
# HELMET Content Security Policy
# Path to a json file containing HELMET `contentSecurityPolicy` directives
# Docs: https://helmetjs.github.io/#reference
#
# Example config:
# {
# "img-src": ["'self'", "data:"],
# "script-src": ["'self'", "'unsafe-inline'"],
# "script-src-attr": ["'self'", "'unsafe-inline'"]
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=
# This location is for server logs with classical UNIX logrotate behavior
LOG_LOCATION=./sasjs_root/logs
# 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 and js
# options: [sas,js|js,sas|sas|js] default:sas
RUN_TIMES=
```
## Persisting the Session
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
1. Linux Background Job
2. NPM package `pm2`
### Background Job
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
### PM2
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
```bash
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
export PORT=5001
export SASJS_ROOT=./sasjs_root
pm2 start api-linux
```
To get the logs (and some useful commands):
```bash
pm2 [list|ls|status]
pm2 logs
pm2 logs --lines 200
```
Managing processes:
```
pm2 restart app_name
pm2 reload app_name
pm2 stop app_name
pm2 delete app_name
```
Instead of `app_name` you can pass:
- `all` to act on all processes
- `id` to act on a specific process id
## Server Version
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
- CLIENTID: `clientID1`
- USERNAME: `secretuser`
- 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!
- Provide path to SAS9 executable.
- Provide `SASjsServer` hostname and port (eg `localhost:5000`).

View File

@@ -1,89 +0,0 @@
<mxfile host="65bd71144e">
<diagram id="HJy_QFGaI9JSrArARLup" name="Page-1">
<mxGraphModel dx="1908" dy="2140" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="4" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
<mxGeometry x="-360" y="-120" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="7" value="SASjs Server" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;fontSize=30;" vertex="1" parent="1">
<mxGeometry x="30" y="-150" width="360" height="850" as="geometry"/>
</mxCell>
<mxCell id="8" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="23" as="sourcePoint"/>
<mxPoint x="115" y="22.586363636363558" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="11" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;span style=&quot;color: #a31515&quot;&gt;/SASjsApi/auth/authorize&lt;br&gt;(username,password,clientId)&lt;/span&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="14" value="" style="edgeStyle=none;html=1;exitX=-0.002;exitY=0.874;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="110" y="80" as="sourcePoint"/>
<mxPoint x="-340" y="80" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="16" value="&lt;font color=&quot;#a31515&quot; face=&quot;menlo, monaco, courier new, monospace&quot;&gt;&lt;span style=&quot;font-size: 12px&quot;&gt;`code`&lt;/span&gt;&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="14">
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="21" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
<mxGeometry x="-360" y="545" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="22" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="30">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="165" as="sourcePoint"/>
<mxPoint x="115" y="165" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="23" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; line-height: 18px&quot;&gt;&lt;span style=&quot;color: #a31515&quot;&gt;/SASjsApi/auth/token&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;color: #a31515&quot;&gt;(clientId,code)&lt;/span&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="22">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="edgeStyle=none;html=1;exitX=0.009;exitY=0.905;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="30">
<mxGeometry relative="1" as="geometry">
<mxPoint x="210" y="222.5" as="sourcePoint"/>
<mxPoint x="-340" y="223" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="&lt;font color=&quot;#a31515&quot; face=&quot;menlo, monaco, courier new, monospace&quot;&gt;&lt;span style=&quot;font-size: 12px&quot;&gt;`&lt;/span&gt;&lt;/font&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;accessToken&lt;/span&gt;&lt;span style=&quot;font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace&quot;&gt;` &amp;amp; `&lt;/span&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;refreshToken&lt;/span&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;`&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="24">
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;" edge="1" parent="1" source="21" target="4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="40" y="240" as="sourcePoint"/>
<mxPoint x="90" y="190" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="28" value="&lt;span&gt;Validates&lt;/span&gt;&lt;br&gt;&lt;span&gt;username/password/clientId&lt;/span&gt;&lt;br&gt;&lt;span&gt;and issue short&lt;/span&gt;&lt;br&gt;&lt;span&gt;Authorization code&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="115" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="30" value="Validates&lt;br&gt;clientId &amp;amp; authorization code&lt;br&gt;and issue&lt;br&gt;Access Token &amp;amp; Refresh Token" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="115" y="140" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="32" value="Protected APIs&lt;br&gt;Authenticate requests &lt;br&gt;with provided Bearer Token" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="50" y="280" width="320" height="400" as="geometry"/>
</mxCell>
<mxCell id="33" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.373;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="32">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="432.5" as="sourcePoint"/>
<mxPoint x="-10" y="430" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="34" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; line-height: 18px&quot;&gt;&lt;font color=&quot;#a31515&quot;&gt;Request with Access Token&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,6 +0,0 @@
build
coverage
node_modules
public
web
Dockerfile

View File

@@ -1,24 +0,0 @@
MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem
CERT_CHAIN=certificate.pem
CA_ROOT=fullchain.pem
PORT=[5000] default value is 5000
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
HELMET_COEP=[true|false] if omitted HELMET default will be used
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
SASJS_ROOT=./sasjs_root
LOG_FORMAT_MORGAN=common
LOG_LOCATION=./sasjs_root/logs

View File

@@ -1 +0,0 @@
v16.15.1

View File

@@ -1,13 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": ["run-script", "start"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
}
]
}

View File

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

17659
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,108 +0,0 @@
{
"name": "api",
"version": "0.0.2",
"description": "Api of SASjs server",
"main": "./src/server.ts",
"scripts": {
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
"prestart": "npm run initial",
"prebuild": "npm run initial",
"start": "NODE_ENV=development nodemon ./src/server.ts",
"start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc",
"postbuild": "npm run copy:files",
"swagger": "tsoa spec",
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
"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}\"",
"exe": "npm run build && pkg .",
"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/",
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
},
"bin": "./build/src/server.js",
"pkg": {
"assets": [
"./build/public/**/*",
"./build/sasjsbuild/**/*",
"./build/sasjscore/**/*",
"./web/build/**/*"
],
"targets": [
"node16-linux-x64",
"node16-macos-x64",
"node16-win-x64"
],
"outputPath": "../executables"
},
"release": {
"branches": [
"main"
]
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.31.3",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"helmet": "^5.0.2",
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"url": "^0.10.3"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
"@types/jsonwebtoken": "^8.5.5",
"@types/mongoose-sequence": "^3.0.6",
"@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7",
"pkg": "5.6.0",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",
"tsoa": "3.14.1",
"typescript": "^4.3.2"
},
"nodemonConfig": {
"ignore": [
"sasjs_root/**/*"
]
}
}

View File

@@ -1,50 +0,0 @@
window.onload = function () {
// Build a system
var url = window.location.search.match(/url=([^&]+)/)
if (url && url.length > 1) {
url = decodeURIComponent(url[1])
} else {
url = window.location.origin
}
var options = {
customOptions: {
url: '/swagger.yaml',
requestInterceptor: function (request) {
request.credentials = 'include'
var cookie = document.cookie
var startIndex = cookie.indexOf('XSRF-TOKEN')
var csrf = cookie.slice(startIndex + 11).split('; ')[0]
request.headers['X-XSRF-TOKEN'] = csrf
return request
}
}
}
url = options.swaggerUrl || url
var urls = options.swaggerUrls
var customOptions = options.customOptions
var spec1 = options.swaggerDoc
var swaggerOptions = {
spec: spec1,
url: url,
urls: urls,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: 'StandaloneLayout'
}
for (var attrname in customOptions) {
swaggerOptions[attrname] = customOptions[attrname]
}
var ui = SwaggerUIBundle(swaggerOptions)
if (customOptions.oauth) {
ui.initOAuth(customOptions.oauth)
}
if (customOptions.authAction) {
ui.authActions.authorize(customOptions.authAction)
}
window.ui = ui
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +0,0 @@
const inputElement = document.getElementById('fileId')
document.getElementById('uploadButton').addEventListener('click', function () {
inputElement.click()
})
inputElement.addEventListener(
'change',
function () {
const fileList = this.files /* now you can work with the file list */
updateFileUploadMessage('Requesting ...')
const file = fileList[0]
const formData = new FormData()
formData.append('file', file)
axios
.post('/SASjsApi/drive/deploy/upload', formData)
.then((res) => res.data)
.then((data) => {
return (
data.message +
'\nstreamServiceName: ' +
data.streamServiceName +
'\nrefreshing page once alert box closes.'
)
})
.then((message) => {
alert(message)
location.reload()
})
.catch((error) => {
alert(error.response.data)
resetFileUpload()
updateFileUploadMessage('Upload New App')
})
},
false
)
function updateFileUploadMessage(message) {
document.getElementById('uploadMessage').innerHTML = message
}
function resetFileUpload() {
inputElement.value = null
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F6E40C;}
</style>
<rect id="XMLID_1_" width="32" height="32"/>
<g id="XMLID_654_">
<path id="XMLID_656_" class="st0" d="M27.9,17.4c-1.1,0-2.1,0-3,0c-1.2,0-2.3,0-3.5,0c-0.5,0-0.7,0.2-0.6,0.7c0,2.1,0,4.3,0,6.4
c0,0.5-0.2,0.8-0.6,1c-2.5,1.4-4.9,2.8-7.3,4.3c-0.4,0.2-0.6,0.2-1,0c-2.4-1.4-4.9-2.9-7.3-4.3c-0.2-0.1-0.5-0.5-0.5-0.7
c0-3.2,0-6.4,0-9.6c0-0.1,0-0.1,0.1-0.3c0.3,0,0.5,0,0.8,0c1.9,0,3.7,0,5.6,0c0.6,0,0.7-0.2,0.7-0.7c0-2.1,0-4.2,0-6.4
c0-0.5,0.1-0.8,0.6-1.1c2.5-1.4,4.9-2.9,7.3-4.3c0.2-0.1,0.6-0.1,0.9,0c2.5,1.4,5,2.9,7.5,4.4c0.2,0.1,0.4,0.4,0.4,0.6
C27.9,10.6,27.9,13.9,27.9,17.4z M20.8,14.8c1.4,0,2.7,0,4,0c0.5,0,0.7-0.2,0.7-0.7c0-1.7,0-3.3,0-5c0-0.5-0.2-0.7-0.6-1
c-1.6-0.9-3.2-1.9-4.8-2.8c-0.2-0.1-0.7-0.1-0.9,0c-1.6,0.9-3.2,1.9-4.8,2.8c-0.4,0.2-0.6,0.5-0.6,1c0,3.2,0,6.3,0,9.5
c0,1.9,0,1.9-1.9,1.9c-0.4,0-0.6-0.1-0.6-0.6c0-0.6,0-1.3,0-1.9c0-0.5-0.2-0.6-0.6-0.6c-1.1,0-2.2,0-3.3,0c-0.5,0-0.7,0.2-0.7,0.7
c0,1.6,0,3.3,0,4.9c0,0.5,0.2,0.8,0.6,1c1.6,0.9,3.2,1.9,4.8,2.8c0.2,0.1,0.7,0.1,0.9,0c1.6-0.9,3.2-1.9,4.8-2.8
c0.4-0.2,0.6-0.5,0.6-1c0-3.1,0-6.1,0-9.2c0-1.9,0-1.9,1.9-1.9c0.5,0,0.7,0.2,0.7,0.7C20.8,13.3,20.8,14,20.8,14.8z"/>
<path id="XMLID_655_" class="st0" d="M18,2.1l-6.8,3.9V2.7c0-0.3,0.3-0.6,0.6-0.6H18z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +0,0 @@
import path from 'path'
import {
CompileTree,
createFile,
loadDependenciesFile,
readFile,
SASJsFileType
} from '@sasjs/utils'
import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
const compiledSystemInit = async (systemInit: string) =>
'options ps=max;\n' +
(await loadDependenciesFile({
fileContent: systemInit,
type: SASJsFileType.job,
programFolders: [],
macroFolders: [],
buildSourceFolder: '',
binaryFolders: [],
macroCorePath,
compileTree: new CompileTree('') // dummy compileTree
}))
const createSysInitFile = async () => {
const systemInitContent = await readFile(
path.join(__dirname, 'systemInit.sas')
)
await createFile(
path.join(sysInitCompiledPath),
await compiledSystemInit(systemInitContent)
)
}
createSysInitFile()

View File

@@ -1,36 +0,0 @@
import path from 'path'
import {
asyncForEach,
copy,
createFile,
createFolder,
deleteFolder,
listFilesInFolder
} from '@sasjs/utils'
import {
apiRoot,
sasJSCoreMacros,
sasJSCoreMacrosInfo
} from '../src/utils/file'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
export const copySASjsCore = async () => {
await deleteFolder(sasJSCoreMacros)
await createFolder(sasJSCoreMacros)
const foldersToCopy = ['base', 'ddl', 'fcmp', 'lua', 'server']
await asyncForEach(foldersToCopy, async (coreSubFolder) => {
const coreSubFolderPath = path.join(macroCorePath, coreSubFolder)
await copy(coreSubFolderPath, sasJSCoreMacros)
})
const fileNames = await listFilesInFolder(sasJSCoreMacros)
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
}
copySASjsCore()

View File

@@ -1,16 +0,0 @@
/**
@file
@brief The systemInit program
@details This program is inserted into every sasjs/server program invocation,
_before_ any user-provided content.
A number of useful CORE macros are also compiled below, so that they can be
available by default for Stored Programs.
Note that the full CORE library is available to sessions in SASjs Studio.
<h4> SAS Macros </h4>
@li mfs_httpheader.sas
@li ms_webout.sas
**/

View File

@@ -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(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
}

View File

@@ -1,32 +0,0 @@
import { Express } from 'express'
import mongoose from 'mongoose'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import { ModeType } from '../utils'
import { cookieOptions } from '../app'
export const configureExpressSession = (app: Express) => {
const { MODE } = process.env
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
if (process.env.NODE_ENV !== 'test') {
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
collectionName: 'sessions'
})
}
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
})
)
}
}

View File

@@ -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
})
console.log('Writing Logs to :', path.join(logsFolder, filename))
options = { stream: accessLogStream }
}
// setup the logger
app.use(morgan(LOG_FORMAT_MORGAN as string, options))
}

View File

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

View File

@@ -1,4 +0,0 @@
export * from './configureCors'
export * from './configureExpressSession'
export * from './configureLogger'
export * from './configureSecurity'

View File

@@ -1,96 +0,0 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import csrf from 'csurf'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import {
copySASjsCore,
getWebBuildFolder,
instantiateLogger,
loadAppStreamConfig,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders,
verifyEnvVariables
} from './utils'
import {
configureCors,
configureExpressSession,
configureLogger,
configureSecurity
} from './app-modules'
dotenv.config()
instantiateLogger()
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
const { PROTOCOL } = process.env
export const cookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
/***********************************
* CSRF Protection *
***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions })
const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack)
res.status(500).send('Something broke!')
}
export default setProcessVariables().then(async () => {
app.use(cookieParser())
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')))
await setupFolders()
await copySASjsCore()
// loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc
const { setupRoutes } = await import('./routes/setupRoutes')
setupRoutes(app)
await loadAppStreamConfig()
// should be served after setting up web route
// index.html needs to be injected with some js script.
app.use(express.static(getWebBuildFolder()))
app.use(onError)
return app
})

View File

@@ -1,146 +0,0 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import {
generateAccessToken,
generateRefreshToken,
removeTokensInDB,
saveTokensInDB
} from '../utils'
@Route('SASjsApi/auth')
@Tags('Auth')
export class AuthController {
static authCodes: { [key: string]: { [key: string]: string } } = {}
static saveCode = (userId: number, clientId: string, code: string) => {
if (AuthController.authCodes[userId])
return (AuthController.authCodes[userId][clientId] = code)
AuthController.authCodes[userId] = { [clientId]: code }
return AuthController.authCodes[userId][clientId]
}
static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId]
/**
* @summary Accepts client/auth code and returns access/refresh tokens
*
*/
@Example<TokenResponse>({
accessToken: 'someRandomCryptoString',
refreshToken: 'someRandomCryptoString'
})
@Post('/token')
public async token(@Body() body: TokenPayload): Promise<TokenResponse> {
return token(body)
}
/**
* @summary Returns new access/refresh tokens
*
*/
@Example<TokenResponse>({
accessToken: 'someRandomCryptoString',
refreshToken: 'someRandomCryptoString'
})
@Security('bearerAuth')
@Post('/refresh')
public async refresh(
@Query() @Hidden() data?: InfoJWT
): Promise<TokenResponse> {
return refresh(data!)
}
/**
* @summary Logout terminate access/refresh tokens and returns nothing
*
*/
@Security('bearerAuth')
@Post('/logout')
public async logout(@Query() @Hidden() data?: InfoJWT) {
return logout(data!)
}
}
const token = async (data: any): Promise<TokenResponse> => {
const { clientId, code } = data
const userInfo = await verifyAuthCode(clientId, code)
if (!userInfo) throw new Error('Invalid Auth Code')
if (AuthController.authCodes[userInfo.userId][clientId] !== code)
throw new Error('Invalid Auth Code')
AuthController.deleteCode(userInfo.userId, clientId)
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
return { accessToken, refreshToken }
}
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(
userInfo.userId,
userInfo.clientId,
accessToken,
refreshToken
)
return { accessToken, refreshToken }
}
const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
interface TokenPayload {
/**
* Client ID
* @example "clientID1"
*/
clientId: string
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}
interface TokenResponse {
/**
* Access Token
* @example "someRandomCryptoString"
*/
accessToken: string
/**
* Refresh Token
* @example "someRandomCryptoString"
*/
refreshToken: string
}
const verifyAuthCode = async (
clientId: string,
code: string
): Promise<InfoJWT | undefined> => {
return new Promise((resolve) => {
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
if (err) return resolve(undefined)
const clientInfo: InfoJWT = {
clientId: data?.clientId,
userId: data?.userId
}
if (clientInfo.clientId === clientId) {
return resolve(clientInfo)
}
return resolve(undefined)
})
})
}

View File

@@ -1,44 +0,0 @@
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
import Client, { ClientPayload } from '../model/Client'
@Security('bearerAuth')
@Route('SASjsApi/client')
@Tags('Client')
export class ClientController {
/**
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
*
*/
@Example<ClientPayload>({
clientId: 'someFormattedClientID1234',
clientSecret: 'someRandomCryptoString'
})
@Post('/')
public async createClient(
@Body() body: ClientPayload
): Promise<ClientPayload> {
return createClient(body)
}
}
const createClient = async (data: any): Promise<ClientPayload> => {
const { clientId, clientSecret } = data
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId })
if (clientExist) throw new Error('Client ID already exists.')
// Create a new client
const client = new Client({
clientId,
clientSecret
})
const savedClient = await client.save()
return {
clientId: savedClient.clientId,
clientSecret: savedClient.clientSecret
}
}

View File

@@ -1,78 +0,0 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.'
import {
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray,
RunTimeType
} from '../utils'
interface ExecuteCodePayload {
/**
* Code of program
* @example "* Code HERE;"
*/
code: string
/**
* runtime for program
* @example "js"
*/
runTime: RunTimeType
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
*/
@Post('/execute')
public async executeCode(
@Request() request: express.Request,
@Body() body: ExecuteCodePayload
): Promise<ExecuteReturnJsonResponse> {
return executeCode(request, body)
}
}
const executeCode = async (
req: express.Request,
{ code, runTime }: ExecuteCodePayload
) => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
try {
const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
returnJson: true,
runTime: runTime
})) as ExecuteReturnJson
return {
status: 'success',
_webout: webout as string,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -1,574 +0,0 @@
import path from 'path'
import express, { Express } from 'express'
import {
Security,
Request,
Route,
Tags,
Example,
Post,
Body,
Response,
Query,
Get,
Patch,
UploadedFile,
FormField,
Delete,
Hidden
} from 'tsoa'
import {
fileExists,
moveFile,
createFolder,
deleteFile as deleteFileOnSystem,
deleteFolder as deleteFolderOnSystem,
folderExists,
listFilesInFolder,
listSubFoldersInFolder,
isFolder,
FileTree,
isFileTree
} from '@sasjs/utils'
import { createFileTree, ExecutionController, getTreeExample } from './internal'
import { TreeNode } from '../types'
import { getFilesFolder } from '../utils'
interface DeployPayload {
appLoc: string
streamWebFolder?: string
fileTree: FileTree
}
interface DeployResponse {
status: string
message: string
streamServiceName?: string
example?: FileTree
}
interface GetFileResponse {
status: string
fileContent?: string
message?: string
}
interface GetFileTreeResponse {
status: string
tree: TreeNode
}
interface FileFolderResponse {
status: 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 successDeployResponse: DeployResponse = {
status: 'success',
message: 'Files deployed successfully to @sasjs/server.'
}
const invalidDeployFormatResponse: DeployResponse = {
status: 'failure',
message: 'Provided not supported data format.',
example: fileTreeExample
}
const execDeployErrorResponse: DeployResponse = {
status: 'failure',
message: 'Deployment failed!'
}
@Security('bearerAuth')
@Route('SASjsApi/drive')
@Tags('Drive')
export class DriveController {
/**
* @summary Creates/updates files within SASjs Drive using provided payload.
*
*/
@Example<DeployResponse>(successDeployResponse)
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
@Post('/deploy')
public async deploy(@Body() body: DeployPayload): Promise<DeployResponse> {
return deploy(body)
}
/**
* Accepts JSON file and zipped compressed JSON file as well.
* 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)
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
@Post('/deploy/upload')
public async deployUpload(
@UploadedFile() file: Express.Multer.File, // passing here for API docs
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
): Promise<DeployResponse> {
return deploy(body!)
}
/**
*
* @summary Get file from SASjs Drive
* @query _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file"
*/
@Get('/file')
public async getFile(
@Request() request: express.Request,
@Query() _filePath: string
) {
return getFile(request, _filePath)
}
/**
*
* @summary Get folder contents from SASjs Drive
* @query _folderPath Location of SAS program
* @example _folderPath "/Public/somefolder"
*/
@Get('/folder')
public async getFolder(@Query() _folderPath?: string) {
return getFolder(_folderPath)
}
/**
*
* @summary Delete file from SASjs Drive
* @query _filePath Location of file
* @example _filePath "/Public/somefolder/some.file"
*/
@Delete('/file')
public async deleteFile(@Query() _filePath: string) {
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
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Create a file in SASjs Drive
* @param _filePath Location of file
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<FileFolderResponse>({
status: 'success'
})
@Response<FileFolderResponse>(403, 'File already exists', {
status: 'failure',
message: 'File request failed.'
})
@Post('/file')
public async saveFile(
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<FileFolderResponse> {
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
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Modify a file in SASjs Drive
* @param _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<FileFolderResponse>({
status: 'success'
})
@Response<FileFolderResponse>(403, `File doesn't exist`, {
status: 'failure',
message: 'File request failed.'
})
@Patch('/file')
public async updateFile(
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<FileFolderResponse> {
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.
*
*/
@Get('/filetree')
public async getFileTree(): Promise<GetFileTreeResponse> {
return getFileTree()
}
}
const getFileTree = () => {
const tree = new ExecutionController().buildDirectoryTree()
return { status: 'success', tree }
}
const deploy = async (data: DeployPayload) => {
const driveFilesPath = getFilesFolder()
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
const appLocPath = path
.join(getFilesFolder(), ...appLocParts)
.replace(new RegExp('/', 'g'), path.sep)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
if (!isFileTree(data.fileTree)) {
throw { code: 400, ...invalidDeployFormatResponse }
}
await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
throw { code: 500, ...execDeployErrorResponse, ...err }
})
return successDeployResponse
}
const getFile = async (req: express.Request, filePath: string) => {
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get file outside drive.`
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
const extension = path.extname(filePathFull).toLowerCase()
if (extension === '.sas') {
req.res?.setHeader('Content-type', 'text/plain')
}
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
}
const getFolder = async (folderPath?: string) => {
const driveFilesPath = getFilesFolder()
if (folderPath) {
const folderPathFull = path
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get folder outside drive.`
}
if (!(await folderExists(folderPathFull)))
throw {
code: 404,
status: 'Not Found',
message: `Folder doesn't exist.`
}
if (!(await isFolder(folderPathFull)))
throw {
code: 400,
status: 'Bad Request',
message: 'Not a Folder.'
}
const files: string[] = await listFilesInFolder(folderPathFull)
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
return { files, folders }
}
const files: string[] = await listFilesInFolder(driveFilesPath)
const folders: string[] = await listSubFoldersInFolder(driveFilesPath)
return { files, folders }
}
const deleteFile = async (filePath: string) => {
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't delete file outside drive.`
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await deleteFileOnSystem(filePathFull)
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 (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't put file outside drive.`
}
if (await fileExists(filePathFull))
throw {
code: 409,
status: 'Conflict',
message: 'File already exists.'
}
const folderPath = path.dirname(filePathFull)
await createFolder(folderPath)
await moveFile(multerFile.path, filePathFull)
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 (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't modify file outside drive.`
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await moveFile(multerFile.path, filePathFull)
return { status: 'success' }
}

View File

@@ -1,271 +0,0 @@
import {
Security,
Route,
Tags,
Path,
Example,
Get,
Post,
Delete,
Body
} from 'tsoa'
import Group, { GroupPayload } from '../model/Group'
import User from '../model/User'
import { UserResponse } from './user'
export interface GroupResponse {
groupId: number
name: string
description: string
}
export interface GroupDetailsResponse {
groupId: number
name: string
description: string
isActive: boolean
users: UserResponse[]
}
interface GetGroupBy {
groupId?: number
name?: string
}
@Security('bearerAuth')
@Route('SASjsApi/group')
@Tags('Group')
export class GroupController {
/**
* @summary Get list of all groups (groupName and groupDescription). All users can request this.
*
*/
@Example<GroupResponse[]>([
{
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users'
}
])
@Get('/')
public async getAllGroups(): Promise<GroupResponse[]> {
return getAllGroups()
}
/**
* @summary Create a new group. Admin only.
*
*/
@Example<GroupDetailsResponse>({
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Post('/')
public async createGroup(
@Body() body: GroupPayload
): Promise<GroupDetailsResponse> {
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.
* @param groupId The group's identifier
* @example groupId 1234
*/
@Get('{groupId}')
public async getGroup(
@Path() groupId: number
): Promise<GroupDetailsResponse> {
return getGroup({ groupId })
}
/**
* @summary Add a user to a group. Admin task only.
* @param groupId The group's identifier
* @example groupId "1234"
* @param userId The user's identifier
* @example userId "6789"
*/
@Example<GroupDetailsResponse>({
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Post('{groupId}/{userId}')
public async addUserToGroup(
@Path() groupId: number,
@Path() userId: number
): Promise<GroupDetailsResponse> {
return addUserToGroup(groupId, userId)
}
/**
* @summary Remove a user to a group. Admin task only.
* @param groupId The group's identifier
* @example groupId "1234"
* @param userId The user's identifier
* @example userId "6789"
*/
@Example<GroupDetailsResponse>({
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Delete('{groupId}/{userId}')
public async removeUserFromGroup(
@Path() groupId: number,
@Path() userId: number
): Promise<GroupDetailsResponse> {
return removeUserFromGroup(groupId, userId)
}
/**
* @summary Delete a group. Admin task only.
* @param groupId The group's identifier
* @example groupId 1234
*/
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
}
}
const getAllGroups = async (): Promise<GroupResponse[]> =>
await Group.find({})
.select({ _id: 0, groupId: 1, name: 1, description: 1 })
.exec()
const createGroup = async ({
name,
description,
isActive
}: 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({
name,
description,
isActive
})
const savedGroup = await group.save()
return {
groupId: savedGroup.groupId,
name: savedGroup.name,
description: savedGroup.description,
isActive: savedGroup.isActive,
users: []
}
}
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne(
findBy,
'groupId name description isActive users -_id'
).populate(
'users',
'id username displayName isAdmin -_id'
)) as unknown as GroupDetailsResponse
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return {
groupId: group.groupId,
name: group.name,
description: group.description,
isActive: group.isActive,
users: group.users
}
}
const addUserToGroup = async (
groupId: number,
userId: number
): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupId, userId, 'addUser')
const removeUserFromGroup = async (
groupId: number,
userId: number
): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupId, userId, 'removeUser')
const updateUsersListInGroup = async (
groupId: number,
userId: number,
action: 'addUser' | 'removeUser'
): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ groupId })
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
const user = await User.findOne({ id: userId })
if (!user)
throw {
code: 404,
status: 'Not Found',
message: 'User not found.'
}
const updatedGroup =
action === 'addUser'
? await group.addUser(user)
: await group.removeUser(user)
if (!updatedGroup)
throw {
code: 400,
status: 'Bad Request',
message: 'Unable to update group.'
}
return {
groupId: updatedGroup.groupId,
name: updatedGroup.name,
description: updatedGroup.description,
isActive: updatedGroup.isActive,
users: updatedGroup.users
}
}

View File

@@ -1,11 +0,0 @@
export * from './auth'
export * from './client'
export * from './code'
export * from './drive'
export * from './group'
export * from './info'
export * from './permission'
export * from './session'
export * from './stp'
export * from './user'
export * from './web'

View File

@@ -1,58 +0,0 @@
import { Route, Tags, Example, Get } from 'tsoa'
import { getAuthorizedRoutes } from '../utils'
export interface AuthorizedRoutesResponse {
URIs: string[]
}
export interface InfoResponse {
mode: string
cors: string
whiteList: string[]
protocol: string
runTimes: string[]
}
@Route('SASjsApi/info')
@Tags('Info')
export class InfoController {
/**
* @summary Get server info (mode, cors, whiteList, protocol).
*
*/
@Example<InfoResponse>({
mode: 'desktop',
cors: 'enable',
whiteList: ['http://example.com', 'http://example2.com'],
protocol: 'http',
runTimes: ['sas', 'js']
})
@Get('/')
public info(): InfoResponse {
const response = {
mode: process.env.MODE ?? 'desktop',
cors:
process.env.CORS ||
(process.env.MODE === 'server' ? 'disable' : 'enable'),
whiteList:
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
protocol: process.env.PROTOCOL ?? 'http',
runTimes: process.runTimes
}
return response
}
/**
* @summary Get authorized routes.
*
*/
@Example<AuthorizedRoutesResponse>({
URIs: ['/AppStream', '/SASjsApi/stp/execute']
})
@Get('/authorizedRoutes')
public authorizedRoutes(): AuthorizedRoutesResponse {
const response = {
URIs: getAuthorizedRoutes()
}
return response
}
}

View File

@@ -1,185 +0,0 @@
import path from 'path'
import fs from 'fs'
import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types'
import {
extractHeaders,
getFilesFolder,
HTTPHeaders,
isDebugOn,
RunTimeType
} from '../../utils'
export interface ExecutionVars {
[key: string]: string | number | undefined
}
export interface ExecuteReturnRaw {
httpHeaders: HTTPHeaders
result: string | Buffer
}
export interface ExecuteReturnJson {
httpHeaders: HTTPHeaders
webout: string | Buffer
log?: string
}
interface ExecuteFileParams {
programPath: string
preProgramVariables: PreProgramVars
vars: ExecutionVars
otherArgs?: any
returnJson?: boolean
session?: Session
runTime: RunTimeType
}
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string
}
export class ExecutionController {
async executeFile({
programPath,
preProgramVariables,
vars,
otherArgs,
returnJson,
session,
runTime
}: ExecuteFileParams) {
const program = await readFile(programPath)
return this.executeProgram({
program,
preProgramVariables,
vars,
otherArgs,
returnJson,
session,
runTime
})
}
async executeProgram({
program,
preProgramVariables,
vars,
otherArgs,
returnJson,
session: sessionByFileUpload,
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController = getSessionController(runTime)
const session =
sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true
session.consumed = true
const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt')
const weboutPath = path.join(session.path, 'webout.txt')
const tokenFile = path.join(session.path, 'reqHeaders.txt')
await createFile(weboutPath, '')
await createFile(
tokenFile,
preProgramVariables?.httpHeaders.join('\n') ?? ''
)
await processProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
runTime,
logPath,
otherArgs
)
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
const headersContent = (await fileExists(headersPath))
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') &&
!returnJson && // not a POST Request
!isDebugOn(vars) // Debug is not enabled
const webout = (await fileExists(weboutPath))
? fileResponse
? await readFileBinary(weboutPath)
: await readFile(weboutPath)
: ''
// it should be deleted by scheduleSessionDestroy
session.inUse = false
if (returnJson) {
return {
httpHeaders,
webout,
log: isDebugOn(vars) || session.crashed ? log : undefined
}
}
return {
httpHeaders,
result:
isDebugOn(vars) || session.crashed
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
: webout
}
}
buildDirectoryTree() {
const root: TreeNode = {
name: 'files',
relativePath: '',
absolutePath: getFilesFolder(),
isFolder: true,
children: []
}
const stack = [root]
while (stack.length) {
const currentNode = stack.pop()
if (currentNode) {
currentNode.isFolder = fs
.statSync(currentNode.absolutePath)
.isDirectory()
const children = fs.readdirSync(currentNode.absolutePath)
for (let child of children) {
const absoluteChildPath = path.join(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 childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
isFolder: false,
children: []
}
currentNode.children.push(childNode)
if (fs.statSync(childNode.absolutePath).isDirectory()) {
stack.push(childNode)
}
}
}
}
return root
}
}

View File

@@ -1,71 +0,0 @@
import { Request, RequestHandler } from 'express'
import multer from 'multer'
import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.'
import {
executeProgramRawValidation,
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
export class FileUploadController {
private storage = multer.diskStorage({
destination: function (req: Request, file: any, cb: any) {
//Sending the intercepted files to the sessions subfolder
cb(null, req.sasjsSession?.path)
},
filename: function (req: Request, file: any, cb: any) {
//req_file prefix + unique hash added to sas request files
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
}
})
private upload = multer({ storage: this.storage })
//It will intercept request and generate unique uuid to be used as a subfolder name
//that will store the files uploaded
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
const { error: errB, value: body } = executeProgramRawValidation(req.body)
if (errQ && errB) return res.status(400).send(errB.details[0].message)
const programPath = (query?._program ?? body?._program) as string
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()
// marking consumed true, so that it's not available
// as readySession for any other request
session.consumed = true
req.sasjsSession = session
next()
}
public getMulterUploadObject() {
return this.upload
}
}

View File

@@ -1,242 +0,0 @@
import path from 'path'
import { Session } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath,
RunTimeType
} from '../../utils'
import {
deleteFolder,
createFile,
fileExists,
generateTimestamp,
readFile,
isWindows
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
abstract class SessionController {
protected sessions: Session[] = []
protected getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
protected abstract createSession(): Promise<Session>
public async getSession() {
const readySessions = this.getReadySessions()
const session = readySessions.length
? readySessions[0]
: await this.createSession()
if (readySessions.length < 3) this.createSession()
return session
}
}
export class SASSessionController extends SessionController {
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,
ready: false,
inUse: false,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
// we do not want to leave sessions running forever
// we clean them up after a predefined period, if unused
this.scheduleSessionDestroy(session)
// Place compiled system init code to autoexec
const compiledSystemInitContent = await readFile(sysInitCompiledPath)
// the autoexec file is executed on SAS startup
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
const contentForAutoExec = `/* compiled systemInit */
${compiledSystemInitContent}
/* autoexec */
${autoExecContent}`
await createFile(autoExecPath, contentForAutoExec)
// create empty code.sas as SAS will not start without a SYSIN
const codePath = path.join(session.path, 'code.sas')
await createFile(codePath, '')
// trigger SAS but don't wait for completion - we need to
// update the session array to say that it is currently running
// however we also need a promise so that we can update the
// session array to say that it has (eventually) finished.
// Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc!, [
'-SYSIN',
codePath,
'-LOG',
path.join(session.path, 'log.log'),
'-PRINT',
path.join(session.path, 'output.lst'),
'-WORK',
session.path,
'-AUTOEXEC',
autoExecPath,
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
isWindows() ? '-nologo' : ''
])
.then(() => {
session.completed = true
console.log('session completed', session)
})
.catch((err) => {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
})
// we have a triggered session - add to array
this.sessions.push(session)
// SAS has been triggered but we can't use it until
// the autoexec deletes the code.sas file
await this.waitForSession(session)
return session
}
private async waitForSession(session: Session) {
const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {}
if (session.crashed)
console.log('session crashed! while waiting to be ready', session.crashed)
session.ready = true
}
public async deleteSession(session: Session) {
// remove the temporary files, to avoid buildup
await deleteFolder(session.path)
// remove the session from the session array
this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id
)
}
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
if (session.inUse) {
// adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
}
}
export class JSSessionController extends SessionController {
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,
ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: application/json')
this.sessions.push(session)
return session
}
}
export const getSessionController = (
runTime: RunTimeType
): SASSessionController | JSSessionController => {
if (runTime === RunTimeType.SAS) {
return getSASSessionController()
}
if (runTime === RunTimeType.JS) {
return getJSSessionController()
}
throw new Error('No Runtime is configured')
}
const getSASSessionController = (): SASSessionController => {
if (process.sasSessionController) return process.sasSessionController
process.sasSessionController = new SASSessionController()
return process.sasSessionController
}
const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController()
return process.jsSessionController
}
const autoExecContent = `
data _null_;
/* remove the dummy SYSIN */
length fname $8;
call missing(fname);
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);
/* now wait for the real SYSIN */
slept=0;
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
slept=slept+sleep(0.01,1);
end;
stop;
run;
`

View File

@@ -1,69 +0,0 @@
import { isWindows } 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,
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 = '${
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
}';
const _sasjs_tokenfile = '${
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
}';
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 js code for the file is generated it will be appended to the top of jsCode
if (uploadJSCode.length > 0) {
program = `${uploadJSCode}\n` + program
}
}
return requiredModules + program
}

View File

@@ -1,69 +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,
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 _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="${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 sas code for the file is generated it will be appended to the top of sasCode
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}
}
return program
}

View File

@@ -1,74 +0,0 @@
import path from 'path'
import { getFilesFolder } from '../../utils/file'
import {
createFolder,
createFile,
asyncForEach,
FolderMember,
ServiceMember,
FileMember,
MemberType,
FileTree
} from '@sasjs/utils'
// REFACTOR: export FileTreeCpntroller
export const createFileTree = async (
members: (FolderMember | ServiceMember | FileMember)[],
parentFolders: string[] = []
) => {
const destinationPath = path.join(
getFilesFolder(),
path.join(...parentFolders)
)
await asyncForEach(
members,
async (member: FolderMember | ServiceMember | FileMember) => {
let name = member.name
if (member.type === MemberType.service) name += '.sas'
if (member.type === MemberType.folder) {
await createFolder(path.join(destinationPath, name)).catch((err) =>
Promise.reject({ error: err, failedToCreate: name })
)
await createFileTree(member.members, [...parentFolders, name]).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
} else {
const encoding = member.type === MemberType.file ? 'base64' : undefined
await createFile(
path.join(destinationPath, name),
member.code,
encoding
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
}
}
)
return Promise.resolve()
}
export const getTreeExample = (): FileTree => ({
members: [
{
name: 'jobs',
type: MemberType.folder,
members: [
{
name: 'extract',
type: MemberType.folder,
members: [
{
name: 'makedata1',
type: MemberType.service,
code: '%put Hello World!;'
}
]
}
]
}
]
})

View File

@@ -1,7 +0,0 @@
export * from './deploy'
export * from './Session'
export * from './Execution'
export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './processProgram'

View File

@@ -1,86 +0,0 @@
import path from 'path'
import fs from 'fs'
import { execFileSync } from 'child_process'
import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { RunTimeType } from '../../utils'
import { ExecutionVars, createSASProgram, createJSProgram } from './'
export const processProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
tokenFile: string,
runTime: RunTimeType,
logPath: string,
otherArgs?: any
) => {
if (runTime === RunTimeType.JS) {
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.js')
try {
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = fs.createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
execFileSync(process.nodeLoc!, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.js program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
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.completed) {
await delay(50)
}
}
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -1,331 +0,0 @@
import {
Security,
Route,
Tags,
Path,
Example,
Get,
Post,
Patch,
Delete,
Body
} 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 PrincipalType {
user = 'user',
group = 'group'
}
export enum PermissionSetting {
grant = 'Grant',
deny = 'Deny'
}
interface RegisterPermissionPayload {
/**
* Name of affected resource
* @example "/SASjsApi/code/execute"
*/
uri: string
/**
* The indication of whether (and to what extent) access is provided
* @example "Grant"
*/
setting: PermissionSetting
/**
* 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: PermissionSetting
}
export interface PermissionDetailsResponse {
permissionId: number
uri: string
setting: string
user?: UserResponse
group?: GroupDetailsResponse
}
@Security('bearerAuth')
@Route('SASjsApi/permission')
@Tags('Permission')
export class PermissionController {
/**
* @summary Get list of all permissions (uri, setting and userDetail).
*
*/
@Example<PermissionDetailsResponse[]>([
{
permissionId: 123,
uri: '/SASjsApi/code/execute',
setting: 'Grant',
user: {
id: 1,
username: 'johnSnow01',
displayName: 'John Snow',
isAdmin: false
}
},
{
permissionId: 124,
uri: '/SASjsApi/code/execute',
setting: 'Grant',
group: {
groupId: 1,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
}
}
])
@Get('/')
public async getAllPermissions(): Promise<PermissionDetailsResponse[]> {
return getAllPermissions()
}
/**
* @summary Create a new permission. Admin only.
*
*/
@Example<PermissionDetailsResponse>({
permissionId: 123,
uri: '/SASjsApi/code/execute',
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,
uri: '/SASjsApi/code/execute',
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 (): Promise<PermissionDetailsResponse[]> =>
(await Permission.find({})
.select({
_id: 0,
permissionId: 1,
uri: 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[]
const createPermission = async ({
uri,
setting,
principalType,
principalId
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
const permission = new Permission({
uri,
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({
uri,
user: userInDB._id
})
if (alreadyExists)
throw {
code: 409,
status: 'Conflict',
message: 'Permission already exists with provided URI 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({
uri,
group: groupInDB._id
})
if (alreadyExists)
throw {
code: 409,
status: 'Conflict',
message: 'Permission already exists with provided URI 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,
uri: savedPermission.uri,
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,
uri: 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 })
}

View File

@@ -1,32 +0,0 @@
import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user'
@Security('bearerAuth')
@Route('SASjsApi/session')
@Tags('Session')
export class SessionController {
/**
* @summary Get session info (username).
*
*/
@Example<UserResponse>({
id: 123,
username: 'johnusername',
displayName: 'John',
isAdmin: false
})
@Get('/')
public async session(
@Request() request: express.Request
): Promise<UserResponse> {
return session(request)
}
}
const session = (req: express.Request) => ({
id: req.user!.userId,
username: req.user!.username,
displayName: req.user!.displayName,
isAdmin: req.user!.isAdmin
})

View File

@@ -1,193 +0,0 @@
import express from 'express'
import {
Request,
Security,
Route,
Tags,
Post,
Body,
Get,
Query,
Example
} from 'tsoa'
import {
ExecuteReturnJson,
ExecuteReturnRaw,
ExecutionController,
ExecutionVars
} from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray,
getRunTimeAndFilePath
} from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload {
/**
* Location of SAS program
* @example "/Public/somefolder/some.file"
*/
_program?: string
}
interface IRecordOfAny {
[key: string]: any
}
export interface ExecuteReturnJsonResponse {
status: string
_webout: string | IRecordOfAny
log: LogLine[]
message?: string
httpHeaders: HTTPHeaders
}
@Security('bearerAuth')
@Route('SASjsApi/stp')
@Tags('STP')
export class STPController {
/**
* Trigger a SAS or JS 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 raw _webout content.
* @param _program Location of SAS or JS code
* @example _program "/Projects/myApp/some/program"
*/
@Get('/execute')
public async executeReturnRaw(
@Request() request: express.Request,
@Query() _program: string
): Promise<string | Buffer> {
return executeReturnRaw(request, _program)
}
/**
* Trigger a SAS or JS program using the _program URL parameter.
*
* Accepts URL parameters and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* The response will be a JSON object with the following root attributes:
* log, webout, headers.
*
* The webout attribute will be nested JSON 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.
*
* @summary Execute a Stored Program, return a JSON object
* @param _program Location of SAS or JS code
* @example _program "/Projects/myApp/some/program"
*/
@Example<ExecuteReturnJsonResponse>({
status: 'success',
_webout: 'webout content',
log: [],
httpHeaders: {
'Content-type': 'application/zip',
'Cache-Control': 'public, max-age=1000'
}
})
@Post('/execute')
public async executeReturnJson(
@Request() request: express.Request,
@Body() body?: ExecuteReturnJsonPayload,
@Query() _program?: string
): Promise<ExecuteReturnJsonResponse> {
const program = _program ?? body?._program
return executeReturnJson(request, program!)
}
}
const executeReturnRaw = async (
req: express.Request,
_program: string
): Promise<string | Buffer> => {
const query = req.query as ExecutionVars
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { result, httpHeaders } =
(await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: query,
runTime
})) as ExecuteReturnRaw
// Should over-ride response header for debug
// on GET request to see entire log rendering on browser.
if (isDebugOn(query)) {
httpHeaders['content-type'] = 'text/plain'
}
req.res?.set(httpHeaders)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const executeReturnJson = async (
req: express.Request,
_program: string
): Promise<ExecuteReturnJsonResponse> => {
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { webout, log, httpHeaders } =
(await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, ...req.body },
otherArgs: { filesNamesMap: filesNamesMap },
returnJson: true,
session: req.sasjsSession,
runTime
})) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
try {
weboutRes = JSON.parse(webout as string)
} catch (_) {}
}
return {
status: 'success',
_webout: weboutRes,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -1,343 +0,0 @@
import express from 'express'
import {
Security,
Route,
Tags,
Path,
Query,
Example,
Get,
Post,
Patch,
Delete,
Body,
Hidden,
Request
} from 'tsoa'
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import { GroupResponse } from './group'
export interface UserResponse {
id: number
username: string
displayName: string
isAdmin: boolean
}
export interface UserDetailsResponse {
id: number
displayName: string
username: string
isActive: boolean
isAdmin: boolean
autoExec?: string
groups?: GroupResponse[]
}
@Security('bearerAuth')
@Route('SASjsApi/user')
@Tags('User')
export class UserController {
/**
* @summary Get list of all users (username, displayname). All users can request this.
*
*/
@Example<UserResponse[]>([
{
id: 123,
username: 'johnusername',
displayName: 'John',
isAdmin: false
},
{
id: 456,
username: 'starkusername',
displayName: 'Stark',
isAdmin: true
}
])
@Get('/')
public async getAllUsers(): Promise<UserResponse[]> {
return getAllUsers()
}
/**
* @summary Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.
*
*/
@Example<UserDetailsResponse>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Post('/')
public async createUser(
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
return createUser(body)
}
/**
* 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.
* @param userId The user's identifier
* @example userId 1234
*/
@Get('{userId}')
public async getUser(
@Request() req: express.Request,
@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)
}
/**
* @summary Update user properties - such as displayName. Can be performed either by admins, or the user in question.
* @param userId The user's identifier
* @example userId "1234"
*/
@Example<UserDetailsResponse>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Patch('{userId}')
public async updateUser(
@Path() userId: number,
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
const { MODE } = process.env
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)
}
/**
* @summary Delete a user. Can be performed either by admins, or the user in question.
* @param userId The user's identifier
* @example userId 1234
*/
@Delete('{userId}')
public async deleteUser(
@Path() userId: number,
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser({ id: userId }, isAdmin, body)
}
}
const getAllUsers = async (): Promise<UserResponse[]> =>
await User.find({})
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
.exec()
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive, autoExec } = data
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
// Hash passwords
const hashPassword = User.hashPassword(password)
// Create a new user
const user = new User({
displayName,
username,
password: hashPassword,
isAdmin,
isActive,
autoExec
})
const savedUser = await user.save()
return {
id: savedUser.id,
displayName: savedUser.displayName,
username: savedUser.username,
isActive: savedUser.isActive,
isAdmin: savedUser.isAdmin,
autoExec: savedUser.autoExec
}
}
interface GetUserBy {
id?: number
username?: string
}
const getUser = async (
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 new Error('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 (
findBy: GetUserBy,
data: Partial<UserPayload>
): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive, autoExec } = data
const params: any = { displayName, isAdmin, isActive, autoExec }
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) {
if (
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw new Error('Username already exists.')
}
params.username = username
}
if (password) {
// Hash passwords
params.password = User.hashPassword(password)
}
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
if (!updatedUser)
throw new Error(`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 (
findBy: GetUserBy,
isAdmin: boolean,
{ password }: { password?: string }
) => {
const user = await User.findOne(findBy)
if (!user) throw new Error('User is not found.')
if (!isAdmin) {
const validPass = user.comparePassword(password!)
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne(findBy)
}

View File

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

View File

@@ -1,108 +0,0 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import {
fetchLatestAutoExec,
ModeType,
verifyTokenInDB,
isAuthorizingRoute
} from '../utils'
import { desktopUser } from './desktop'
import { authorize } from './authorize'
export const authenticateAccessToken: RequestHandler = async (
req,
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.sendStatus(401)
}
}
return res.sendStatus(401)
}
authenticateToken(
req,
res,
nextFunction,
process.secrets.ACCESS_TOKEN_SECRET,
'accessToken'
)
}
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
authenticateToken(
req,
res,
next,
process.secrets.REFRESH_TOKEN_SECRET,
'refreshToken'
)
}
const authenticateToken = (
req: Request,
res: Response,
next: NextFunction,
key: string,
tokenType: 'accessToken' | 'refreshToken'
) => {
const { MODE } = process.env
if (MODE === ModeType.Desktop) {
req.user = {
userId: 1234,
clientId: 'desktopModeClientId',
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',
isAdmin: true,
isActive: true
}
req.accessToken = 'desktopModeAccessToken'
return next()
}
const authHeader = req.headers['authorization']
const token = authHeader?.split(' ')[1]
if (!token) return res.sendStatus(401)
jwt.verify(token, key, async (err: any, data: any) => {
if (err) return res.sendStatus(401)
// verify this valid token's entry in DB
const user = await verifyTokenInDB(
data?.userId,
data?.clientId,
token,
tokenType
)
if (user) {
if (user.isActive) {
req.user = user
if (tokenType === 'accessToken') req.accessToken = token
return next()
} else return res.sendStatus(401)
}
return res.sendStatus(401)
})
}

View File

@@ -1,36 +0,0 @@
import { RequestHandler } from 'express'
import User from '../model/User'
import Permission from '../model/Permission'
import { PermissionSetting } from '../controllers/permission'
import { getUri } 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()
const dbUser = await User.findOne({ id: user.userId })
if (!dbUser) return res.sendStatus(401)
const uri = getUri(req)
// find permission w.r.t user
const permission = await Permission.findOne({ uri, user: dbUser._id })
if (permission) {
if (permission.setting === PermissionSetting.grant) return next()
else return res.sendStatus(401)
}
// find permission w.r.t user's groups
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({ uri, group })
if (groupPermission?.setting === PermissionSetting.grant) return next()
}
return res.sendStatus(401)
}

View File

@@ -1,37 +0,0 @@
import { RequestHandler, Request } from 'express'
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
if (MODE === ModeType.Desktop) {
if (!reqAllowedInDesktopMode(req))
return res.status(403).send('Not Allowed while in Desktop Mode.')
}
next()
}
export const desktopUser: RequestUser = {
userId: 12345,
clientId: 'desktop_app',
username: userInfo().username,
displayName: userInfo().username,
isAdmin: true,
isActive: true
}

View File

@@ -1,5 +0,0 @@
export * from './authenticateToken'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'
export * from './authorize'

View File

@@ -1,72 +0,0 @@
import path from 'path'
import { Request } from 'express'
import multer, { FileFilterCallback, Options } from 'multer'
import { blockFileRegex, getUploadsFolder } from '../utils'
const fieldNameSize = 300
const fileSize = 104857600 // 100 MB
const storage = multer.diskStorage({
destination: getUploadsFolder(),
filename: function (
_req: Request,
file: Express.Multer.File,
callback: (error: Error | null, filename: string) => void
) {
callback(
null,
file.fieldname + path.extname(file.originalname) + '-' + Date.now()
)
}
})
const limits: Options['limits'] = {
fieldNameSize,
fileSize
}
const fileFilter: Options['fileFilter'] = (
req: Request,
file: Express.Multer.File,
callback: FileFilterCallback
) => {
const fileExtension = path.extname(file.originalname)
const shouldBlockUpload = blockFileRegex.test(file.originalname)
if (shouldBlockUpload) {
return callback(
new Error(`File extension '${fileExtension}' not acceptable.`)
)
}
const uploadFileSize = parseInt(req.headers['content-length'] ?? '')
if (uploadFileSize > fileSize) {
return callback(
new Error(
`File size is over limit. File limit is: ${fileSize / 1024 / 1024} MB`
)
)
}
callback(null, true)
}
const options: Options = { storage, limits, fileFilter }
const multerInstance = multer(options)
export const multerSingle = (fileName: string, arg: any) => {
const [req, res, next] = arg
const upload = multerInstance.single(fileName)
upload(req, res, function (err) {
if (err instanceof multer.MulterError) {
return res.status(500).send(err.message)
} else if (err) {
return res.status(400).send(err.message)
}
// Everything went fine.
next()
})
}
export default multerInstance

View File

@@ -1,11 +0,0 @@
import { RequestHandler } from 'express'
import { ModeType } from '../utils'
export const verifyAdmin: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return next()
const { user } = req
if (!user?.isAdmin) return res.status(401).send('Admin account required')
next()
}

View File

@@ -1,22 +0,0 @@
import { RequestHandler } from 'express'
// 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
if (!user?.isAdmin) {
let adminAccountRequired: boolean = true
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()
}

View File

@@ -1,27 +0,0 @@
import mongoose, { Schema } from 'mongoose'
export interface ClientPayload {
/**
* Client ID
* @example "someFormattedClientID1234"
*/
clientId: string
/**
* Client Secret
* @example "someRandomCryptoString"
*/
clientSecret: string
}
const ClientSchema = new Schema<ClientPayload>({
clientId: {
type: String,
required: true
},
clientSecret: {
type: String,
required: true
}
})
export default mongoose.model('Client', ClientSchema)

View File

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

View File

@@ -1,106 +0,0 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose)
export interface GroupPayload {
/**
* Name of the group
* @example "DCGroup"
*/
name: string
/**
* Description of the group
* @example "This group represents Data Controller Users"
*/
description: string
/**
* Group should be active or not, defaults to true
* @example "true"
*/
isActive?: boolean
}
interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean
users: Schema.Types.ObjectId[]
}
interface IGroup extends IGroupDocument {
addUser(user: IUser): Promise<GroupDetailsResponse>
removeUser(user: IUser): Promise<GroupDetailsResponse>
hasUser(user: IUser): boolean
}
interface IGroupModel extends Model<IGroup> {}
const groupSchema = new Schema<IGroupDocument>({
name: {
type: String,
required: true,
unique: true
},
description: {
type: String,
default: 'Group description.'
},
isActive: {
type: Boolean,
default: true
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks
groupSchema.post('save', function (group: IGroup, next: Function) {
group.populate('users', 'id username displayName -_id').then(function () {
next()
})
})
// pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function () {
const userIds = this.users
await Promise.all(
userIds.map(async (userId) => {
const user = await User.findById(userId)
user?.removeGroup(this._id)
})
)
})
// Instance Methods
groupSchema.method('addUser', async function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex === -1) {
this.users.push(userObjectId)
user.addGroup(this._id)
}
this.markModified('users')
return this.save()
})
groupSchema.method('removeUser', async function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex > -1) {
this.users.splice(userIdIndex, 1)
user.removeGroup(this._id)
}
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>(
'Group',
groupSchema
)
export default Group

View File

@@ -1,36 +0,0 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
interface IPermissionDocument extends Document {
uri: string
setting: string
permissionId: number
user: Schema.Types.ObjectId
group: Schema.Types.ObjectId
}
interface IPermission extends IPermissionDocument {}
interface IPermissionModel extends Model<IPermission> {}
const permissionSchema = new Schema<IPermissionDocument>({
uri: {
type: String,
required: true
},
setting: {
type: String,
required: true
},
user: { type: Schema.Types.ObjectId, ref: 'User' },
group: { type: Schema.Types.ObjectId, ref: 'Group' }
})
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
export const Permission: IPermissionModel = model<
IPermission,
IPermissionModel
>('Permission', permissionSchema)
export default Permission

View File

@@ -1,137 +0,0 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import bcrypt from 'bcryptjs'
export interface UserPayload {
/**
* Display name for user
* @example "John Snow"
*/
displayName: string
/**
* Username for user
* @example "johnSnow01"
*/
username: string
/**
* Password for user
*/
password: string
/**
* Account should be admin or not, defaults to false
* @example "false"
*/
isAdmin?: boolean
/**
* Account should be active or not, defaults to true
* @example "true"
*/
isActive?: boolean
/**
* User-specific auto-exec code
* @example ""
*/
autoExec?: string
}
interface IUserDocument extends UserPayload, Document {
_id: Schema.Types.ObjectId
id: number
isAdmin: boolean
isActive: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
}
export interface IUser extends IUserDocument {
comparePassword(password: string): boolean
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
}
interface IUserModel extends Model<IUser> {
hashPassword(password: string): string
}
const userSchema = new Schema<IUserDocument>({
displayName: {
type: String,
required: true
},
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
isAdmin: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [
{
clientId: {
type: String,
required: true
},
accessToken: {
type: String,
required: true
},
refreshToken: {
type: String,
required: true
}
}
]
})
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
// Static Methods
userSchema.static('hashPassword', (password: string): string => {
const salt = bcrypt.genSaltSync(10)
return bcrypt.hashSync(password, salt)
})
// Instance Methods
userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true
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 default User

View File

@@ -1,57 +0,0 @@
import express from 'express'
import { AuthController } from '../../controllers/'
import {
authenticateAccessToken,
authenticateRefreshToken
} from '../../middlewares'
import { authorizeValidation, tokenValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()
const controller = new AuthController()
authRouter.post('/token', async (req, res) => {
const { error, value: body } = tokenValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.token(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
try {
const response = await controller.refresh(userInfo)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
try {
await controller.logout(userInfo)
} catch (e) {}
res.sendStatus(204)
})
export default authRouter

View File

@@ -1,20 +0,0 @@
import express from 'express'
import { ClientController } from '../../controllers'
import { registerClientValidation } from '../../utils'
const clientRouter = express.Router()
clientRouter.post('/', async (req, res) => {
const { error, value: body } = registerClientValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const controller = new ClientController()
try {
const response = await controller.createClient(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default clientRouter

View File

@@ -1,31 +0,0 @@
import express from 'express'
import { runCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/'
const runRouter = express.Router()
const controller = new CodeController()
runRouter.post('/execute', async (req, res) => {
const { error, value: body } = runCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeCode(req, body)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter

View File

@@ -1,292 +0,0 @@
import express from 'express'
import { deleteFile, readFile } from '@sasjs/utils'
import { publishAppStream } from '../appStream'
import { multerSingle } from '../../middlewares/multer'
import { DriveController } from '../../controllers/'
import {
deployValidation,
extractJSONFromZip,
extractName,
fileBodyValidation,
fileParamValidation,
folderBodyValidation,
folderParamValidation,
isZipFile,
renameBodyValidation
} from '../../utils'
const controller = new DriveController()
const driveRouter = express.Router()
driveRouter.post('/deploy', async (req, res) => {
const { error, value: body } = deployValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.deploy(body)
if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
driveRouter.post(
'/deploy/upload',
(...arg) => multerSingle('file', arg),
async (req, res) => {
if (!req.file) return res.status(400).send('"file" is not present.')
let fileContent: string = ''
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
try {
jsonContent = JSON.parse(fileContent)
} catch (err) {
deleteFile(req.file.path)
return res.status(400).send('File containing invalid JSON content.')
}
const { error, value: body } = deployValidation(jsonContent)
if (error) {
deleteFile(req.file.path)
return res.status(400).send(error.details[0].message)
}
try {
const response = await controller.deployUpload(req.file, body)
if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
} finally {
deleteFile(req.file.path)
}
}
)
driveRouter.get('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
await controller.getFile(req, query._filePath)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.get('/folder', async (req, res) => {
const { error: errQ, value: query } = folderParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.getFolder(query._folderPath)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.delete('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.deleteFile(query._filePath)
res.send(response)
} catch (err: any) {
const statusCode = err.code
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)
}
})
driveRouter.post(
'/file',
(...arg) => multerSingle('file', arg),
async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) {
if (req.file) await deleteFile(req.file.path)
return res.status(400).send(errQ.details[0].message)
}
if (!req.file) return res.status(400).send('"file" is not present.')
try {
const response = await controller.saveFile(
req.file,
query._filePath,
body.filePath
)
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
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(
'/file',
(...arg) => multerSingle('file', arg),
async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) {
if (req.file) await deleteFile(req.file.path)
return res.status(400).send(errQ.details[0].message)
}
if (!req.file) return res.status(400).send('"file" is not present.')
try {
const response = await controller.updateFile(
req.file,
query._filePath,
body.filePath
)
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
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) => {
try {
const response = await controller.getFileTree()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default driveRouter

View File

@@ -1,152 +0,0 @@
import express from 'express'
import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import { getGroupValidation, registerGroupValidation } from '../../utils'
const groupRouter = express.Router()
groupRouter.post(
'/',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const { error, value: body } = registerGroupValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const controller = new GroupController()
try {
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const controller = new GroupController()
try {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const { groupId } = req.params
const controller = new GroupController()
try {
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
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) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.post(
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.addUserToGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.delete(
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.removeUserFromGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.delete(
'/:groupId',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const { groupId } = req.params
const controller = new GroupController()
try {
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
export default groupRouter

View File

@@ -1,65 +0,0 @@
import express from 'express'
import swaggerUi from 'swagger-ui-express'
import {
authenticateAccessToken,
desktopRestrict,
verifyAdmin
} from '../../middlewares'
import infoRouter from './info'
import driveRouter from './drive'
import stpRouter from './stp'
import codeRouter from './code'
import userRouter from './user'
import groupRouter from './group'
import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
import permissionRouter from './permission'
const router = express.Router()
router.use('/info', infoRouter)
router.use('/session', authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter)
router.use(
'/client',
desktopRestrict,
authenticateAccessToken,
verifyAdmin,
clientRouter
)
router.use('/drive', authenticateAccessToken, driveRouter)
router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter)
router.use(
'/permission',
desktopRestrict,
authenticateAccessToken,
permissionRouter
)
router.use(
'/',
swaggerUi.serve,
swaggerUi.setup(undefined, {
swaggerOptions: {
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
}
}
})
)
export default router

View File

@@ -1,26 +0,0 @@
import express from 'express'
import { InfoController } from '../../controllers'
const infoRouter = express.Router()
infoRouter.get('/', async (req, res) => {
const controller = new InfoController()
try {
const response = controller.info()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
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

View File

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

View File

@@ -1,16 +0,0 @@
import express from 'express'
import { SessionController } from '../../controllers'
const sessionRouter = express.Router()
sessionRouter.get('/', async (req, res) => {
const controller = new SessionController()
try {
const response = await controller.session(req)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sessionRouter

View File

@@ -1,247 +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,
AuthController
} from '../../../controllers/'
import { InfoJWT } from '../../../types'
import {
generateAccessToken,
generateAuthCode,
generateRefreshToken,
saveTokensInDB,
verifyTokenInDB
} from '../../../utils'
const clientId = 'someclientID'
const clientSecret = 'someclientSecret'
const user = {
id: 1234,
displayName: 'Test User',
username: 'testUsername',
password: '87654321',
isAdmin: false,
isActive: true
}
describe('auth', () => {
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('token', () => {
const userInfo: InfoJWT = {
clientId,
userId: user.id
}
beforeAll(async () => {
await userController.createUser(user)
})
afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with access and refresh tokens', async () => {
const code = AuthController.saveCode(
userInfo.userId,
userInfo.clientId,
generateAuthCode(userInfo)
)
const res = await request(app)
.post('/SASjsApi/auth/token')
.send({
clientId,
code
})
.expect(200)
expect(res.body).toHaveProperty('accessToken')
expect(res.body).toHaveProperty('refreshToken')
})
it('should respond with Bad Request if code is missing', async () => {
const res = await request(app)
.post('/SASjsApi/auth/token')
.send({
clientId
})
.expect(400)
expect(res.text).toEqual(`"code" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => {
const code = AuthController.saveCode(
userInfo.userId,
userInfo.clientId,
generateAuthCode(userInfo)
)
const res = await request(app)
.post('/SASjsApi/auth/token')
.send({
code
})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if code is invalid', async () => {
const res = await request(app)
.post('/SASjsApi/auth/token')
.send({
clientId,
code: 'InvalidCode'
})
.expect(403)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if clientId is invalid', async () => {
const code = AuthController.saveCode(
userInfo.userId,
userInfo.clientId,
generateAuthCode(userInfo)
)
const res = await request(app)
.post('/SASjsApi/auth/token')
.send({
clientId: 'WrongClientID',
code
})
.expect(403)
expect(res.body).toEqual({})
})
})
describe('refresh', () => {
let refreshToken: string
let currentUser: any
beforeEach(async () => {
currentUser = await userController.createUser(user)
refreshToken = generateRefreshToken({
clientId,
userId: currentUser.id
})
await saveTokensInDB(
currentUser.id,
clientId,
'accessToken',
refreshToken
)
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with new access and refresh tokens', async () => {
const res = await request(app)
.post('/SASjsApi/auth/refresh')
.auth(refreshToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toHaveProperty('accessToken')
expect(res.body).toHaveProperty('refreshToken')
// cannot use same refresh again
const resWithError = await request(app)
.post('/SASjsApi/auth/refresh')
.auth(refreshToken, { type: 'bearer' })
.send()
.expect(401)
expect(resWithError.body).toEqual({})
})
})
describe('logout', () => {
let accessToken: string
let currentUser: any
beforeEach(async () => {
currentUser = await userController.createUser(user)
accessToken = generateAccessToken({
clientId,
userId: currentUser.id
})
await saveTokensInDB(
currentUser.id,
clientId,
accessToken,
'refreshToken'
)
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond no content and remove access/refresh tokens from DB', async () => {
const res = await request(app)
.delete('/SASjsApi/auth/logout')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(204)
expect(res.body).toEqual({})
expect(
await verifyTokenInDB(
currentUser.id,
clientId,
accessToken,
'accessToken'
)
).toBeUndefined()
})
})
})

View File

@@ -1,160 +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/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
const client = {
clientId: 'someclientID',
clientSecret: 'someclientSecret'
}
const adminUser = {
displayName: 'Test Admin',
username: 'testAdminUsername',
password: '12345678',
isAdmin: true,
isActive: true
}
const newClient = {
clientId: 'newClientID',
clientSecret: 'newClientSecret'
}
describe('client', () => {
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())
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
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 () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
await collection.deleteMany({})
})
it('should respond with new client', async () => {
const res = await request(app)
.post('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send(newClient)
.expect(200)
expect(res.body.clientId).toEqual(newClient.clientId)
expect(res.body.clientSecret).toEqual(newClient.clientSecret)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.post('/SASjsApi/client')
.send(newClient)
.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 1',
username: 'username1',
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)
.post('/SASjsApi/client')
.auth(accessToken, { type: 'bearer' })
.send(newClient)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if clientId is already present', async () => {
await clientController.createClient(newClient)
const res = await request(app)
.post('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send(newClient)
.expect(403)
expect(res.text).toEqual('Error: Client ID already exists.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...newClient,
clientId: undefined
})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientSecret is missing', async () => {
const res = await request(app)
.post('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...newClient,
clientSecret: undefined
})
.expect(400)
expect(res.text).toEqual(`"clientSecret" is required`)
expect(res.body).toEqual({})
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
some code of sas

View File

@@ -1 +0,0 @@
some code of sas

View File

@@ -1,661 +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, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
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 group = {
name: 'dcgroup1',
description: 'DC group for testing purposes.'
}
const userController = new UserController()
const groupController = new GroupController()
describe('group', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
adminAccessToken = await generateSaveTokenAndCreateUser()
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('create', () => {
afterEach(async () => {
await deleteAllGroups()
})
it('should respond with new group', async () => {
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send(group)
.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 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 () => {
const res = await request(app).post('/SASjsApi/group').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: 'create' + user.username
})
const res = await request(app)
.post('/SASjsApi/group')
.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 name is missing', async () => {
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...group,
name: undefined
})
.expect(400)
expect(res.text).toEqual(`"name" is required`)
expect(res.body).toEqual({})
})
})
describe('delete', () => {
afterEach(async () => {
await deleteAllGroups()
})
it('should respond with OK when admin user requests', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toEqual({})
})
it(`should delete group's reference from users' groups array`, 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)
.delete(`/SASjsApi/group/1234`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/1234')
.send()
.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', async () => {
const dbGroup = await groupController.createGroup(group)
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'delete' + user.username
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
})
describe('get', () => {
afterEach(async () => {
await deleteAllGroups()
})
it('should respond with group', async () => {
const { groupId } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/${groupId}`)
.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: 'get' + user.username
})
const { groupId } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/${groupId}`)
.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/1234')
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.get('/SASjsApi/group/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Group not found.')
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', () => {
afterEach(async () => {
await deleteAllGroups()
})
it('should respond with all groups', async () => {
await groupController.createGroup(group)
const res = await request(app)
.get('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toEqual([
{
groupId: expect.anything(),
name: group.name,
description: group.description
}
])
})
it('should respond with all groups when access token is not of an admin account', async () => {
await groupController.createGroup(group)
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'getAllrandomUser'
})
const res = await request(app)
.get('/SASjsApi/group')
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(200)
expect(res.body).toEqual([
{
groupId: expect.anything(),
name: group.name,
description: group.description
}
])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/group').send().expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
})
describe('AddUser', () => {
afterEach(async () => {
await deleteAllGroups()
})
it('should respond with group having new user in it', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser(user)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.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([
{
id: expect.anything(),
username: user.username,
displayName: user.displayName
}
])
})
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 () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'addUserRandomUser'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.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([
{
id: expect.anything(),
username: 'addUserRandomUser',
displayName: user.displayName
}
])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.post('/SASjsApi/group/123/123')
.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: 'addUser' + user.username
})
const res = await request(app)
.post('/SASjsApi/group/123/123')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.post('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})
describe('RemoveUser', () => {
afterEach(async () => {
await deleteAllGroups()
})
it('should respond with group having user removed from it', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'removeUserRandomUser'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.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 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 Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
.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: 'removeUser' + user.username
})
const res = await request(app)
.delete('/SASjsApi/group/123/123')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})
})
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 deleteAllGroups = async () => {
const { collections } = mongoose.connection
const collection = collections['groups']
await collection.deleteMany({})
}

View File

@@ -1,20 +0,0 @@
import { Express } from 'express'
import request from 'supertest'
import appPromise from '../../../app'
describe('Info', () => {
let app: Express
beforeAll(async () => {
app = await appPromise
})
it('should should return configured information of the server instance', async () => {
const res = await request(app).get('/SASjsApi/info').expect(200)
expect(res.body.mode).toEqual('server')
expect(res.body.cors).toEqual('disable')
expect(res.body.whiteList).toEqual([])
expect(res.body.protocol).toEqual('http')
})
})

View File

@@ -1,571 +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,
ClientController,
PermissionController,
PrincipalType,
PermissionSetting
} 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 = {
uri: '/SASjsApi/code/execute',
setting: PermissionSetting.grant,
principalType: PrincipalType.user,
principalId: 123
}
const group = {
name: 'DCGroup1',
description: 'DC group for testing purposes.'
}
const userController = new UserController()
const groupController = new GroupController()
const clientController = new ClientController()
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.uri).toEqual(permission.uri)
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.uri).toEqual(permission.uri)
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 even if user has permission', async () => {
const accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
uri: '/SASjsApi/permission',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
})
const res = await request(app)
.post('/SASjsApi/permission')
.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 uri is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
uri: undefined
})
.expect(400)
expect(res.text).toEqual(`"uri" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if uri is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
uri: '/some/random/api/endpoint'
})
.expect(400)
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 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 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 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 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 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'
})
.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 URI 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: '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(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 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 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 not found (404) if permission with provided id does not exists', async () => {
const res = await request(app)
.patch('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' })
.send({
setting: PermissionSetting.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,
uri: '/test-1',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
uri: '/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 all permissions when user is not admin', async () => {
const dbUser = await userController.createUser({
...user,
username: 'get' + user.username
})
const accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
uri: '/SASjsApi/permission',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
})
const res = await request(app)
.get('/SASjsApi/permission/')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toHaveLength(3)
})
})
describe.only('verify', () => {
beforeAll(async () => {
await permissionController.createPermission({
...permission,
uri: '/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({})
}

View File

@@ -1,397 +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,
PermissionSetting,
PrincipalType
} from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
getFilesFolder,
RunTimeType,
generateUniqueFileName,
getSessionsFolder
} from '../../../utils'
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } 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 filesFolder = getFilesFolder()
describe('stp', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let accessToken: string
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({
uri: '/SASjsApi/stp/execute',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
})
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('execute', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
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 () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
})
it('should throw error when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(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 () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
})
it('should throw error when sas program do not exit but js exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(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 () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
})
it('should execute sas program when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(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 () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
})
it('should execute js program when sas program is not present but js program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
})
})
})
const generateSaveTokenAndCreateUser = async (
someUser: any
): Promise<string> => {
const userController = new UserController()
const dbUser = await userController.createUser(someUser)
return generateAndSaveToken(dbUser.id)
}
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(JSSessionController.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,
ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
return session
}

View File

@@ -1,843 +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, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
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,
autoExec: 'some sas code for auto exec;'
}
const controller = new UserController()
describe('user', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('create', () => {
let adminAccessToken: string
beforeEach(async () => {
adminAccessToken = await generateSaveTokenAndCreateUser()
})
afterEach(async () => {
await deleteAllUsers()
})
it('should respond with new user', async () => {
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send(user)
.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 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 () => {
const res = await request(app)
.post('/SASjsApi/user')
.send(user)
.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 dbUser = await controller.createUser(user)
const accessToken = generateAccessToken({
clientId,
userId: dbUser.id
})
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
const res = await request(app)
.post('/SASjsApi/user')
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
await controller.createUser(user)
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send(user)
.expect(403)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if username is missing', async () => {
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...user,
username: undefined
})
.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/user')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...user,
password: undefined
})
.expect(400)
expect(res.text).toEqual(`"password" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if displayName is missing', async () => {
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...user,
displayName: undefined
})
.expect(400)
expect(res.text).toEqual(`"displayName" is required`)
expect(res.body).toEqual({})
})
})
describe('update', () => {
let adminAccessToken: string
beforeEach(async () => {
adminAccessToken = await generateSaveTokenAndCreateUser()
})
afterEach(async () => {
await deleteAllUsers()
})
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/${dbUser.id}`)
.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/${dbUser.id}`)
.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/${dbUser.id}`)
.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/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 Forbidden 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/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
expect(res.text).toEqual('Error: Username already exists.')
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 Forbidden 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(403)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
})
})
describe('delete', () => {
let adminAccessToken: string
beforeEach(async () => {
adminAccessToken = await generateSaveTokenAndCreateUser()
})
afterEach(async () => {
await deleteAllUsers()
})
it('should respond with OK when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.id}`)
.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/${dbUser.id}`)
.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/${dbUser.id}`)
.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/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)
.delete(`/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 Forbidden 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/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
expect(res.text).toEqual('Error: Invalid password.')
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 Forbidden 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(403)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
})
})
describe('get', () => {
let adminAccessToken: string
beforeEach(async () => {
adminAccessToken = await generateSaveTokenAndCreateUser()
})
afterEach(async () => {
await deleteAllUsers()
})
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/${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 userId = dbUser.id
const res = await request(app)
.get(`/SASjsApi/user/${userId}`)
.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)
expect(res.body.groups).toEqual([])
})
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 userId = 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).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 () => {
const res = await request(app)
.get('/SASjsApi/user/1234')
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if userId is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
expect(res.text).toEqual('Error: User is not found.')
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 Forbidden 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(403)
expect(res.text).toEqual('Error: User is not found.')
expect(res.body).toEqual({})
})
})
})
describe('getAll', () => {
let adminAccessToken: string
beforeEach(async () => {
adminAccessToken = await generateSaveTokenAndCreateUser()
})
afterEach(async () => {
await deleteAllUsers()
})
it('should respond with all users', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toEqual([
{
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
id: expect.anything(),
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin
}
])
})
it('should respond with all users when access token is not of an admin account', async () => {
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'randomUser'
})
const res = await request(app)
.get('/SASjsApi/user')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toEqual([
{
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
id: expect.anything(),
username: 'randomUser',
displayName: user.displayName,
isAdmin: user.isAdmin
}
])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/user').send().expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
})
})
const generateSaveTokenAndCreateUser = async (
someUser?: any
): Promise<string> => {
const dbUser = await controller.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 deleteAllUsers = async () => {
const { collections } = mongoose.connection
const collection = collections['users']
await collection.deleteMany({})
}

View File

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

View File

@@ -1,68 +0,0 @@
import express from 'express'
import { executeProgramRawValidation } from '../../utils'
import { STPController } from '../../controllers/'
import { FileUploadController } from '../../controllers/internal'
const stpRouter = express.Router()
const fileUploadController = new FileUploadController()
const controller = new STPController()
stpRouter.get('/execute', async (req, res) => {
const { error, value: query } = executeProgramRawValidation(req.query)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeReturnRaw(req, query._program)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
stpRouter.post(
'/execute',
fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(),
async (req, res: any) => {
// below validations are moved to preUploadMiddleware
// const { error: errQ, value: query } = executeProgramRawValidation(req.query)
// const { error: errB, value: body } = executeProgramRawValidation(req.body)
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
try {
const response = await controller.executeReturnJson(
req,
req.body,
req.query?._program as string
)
// TODO: investigate if this code is required
// if (response instanceof Buffer) {
// res.writeHead(200, (req as any).sasHeaders)
// return res.end(response)
// }
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
}
)
export default stpRouter

View File

@@ -1,171 +0,0 @@
import express from 'express'
import { UserController } from '../../controllers/'
import {
authenticateAccessToken,
verifyAdmin,
verifyAdminIfNeeded
} from '../../middlewares'
import {
deleteUserValidation,
getUserValidation,
registerUserValidation,
updateUserValidation
} from '../../utils'
const userRouter = express.Router()
userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
const { error, value: body } = registerUserValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
const response = await controller.createUser(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
userRouter.get('/', authenticateAccessToken, async (req, res) => {
const controller = new UserController()
try {
const response = await controller.getAllUsers()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
userRouter.get(
'/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(403).send(err.toString())
}
}
)
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const { userId } = req.params
const controller = new UserController()
try {
const response = await controller.getUser(req, parseInt(userId))
res.send(response)
} catch (err: any) {
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(403).send(err.toString())
}
}
)
userRouter.patch(
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req, res) => {
const { user } = req
const { userId } = req.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.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
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(403).send(err.toString())
}
}
)
userRouter.delete(
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req, res) => {
const { user } = req
const { userId } = req.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.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
export default userRouter

View File

@@ -1,52 +0,0 @@
import { AppStreamConfig } from '../../types'
import { style } from './style'
const defaultAppLogo = '/sasjs-logo.svg'
const singleAppStreamHtml = (
streamServiceName: string,
appLoc: string,
logo?: string
) =>
` <a class="app" href="${streamServiceName}" title="${appLoc}">
<img
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
onerror="this.src = '${defaultAppLogo}';"
/>
${streamServiceName}
</a>`
export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
<html>
<head>
<base href="/AppStream/">
${style}
</head>
<body>
<header>
<a href="/"><img src="/logo.png" alt="logo" class="logo"></a>
<h1>App Stream</h1>
</header>
<div class="app-container">
${Object.entries(appStreamConfig)
.map(([streamServiceName, entry]) =>
singleAppStreamHtml(
streamServiceName,
entry.appLoc,
entry.streamLogo
)
)
.join('')}
<a class="app" title="Upload build.json">
<input id="fileId" type="file" hidden />
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
<img src="/plus.png" />
</button>
<span id="uploadMessage">Upload New App</span>
</a>
</div>
<script src="/axios.min.js"></script>
<script src="/app-streams-script.js"></script>
</body>
</html>`

View File

@@ -1,92 +0,0 @@
import path from 'path'
import express, { Request } from 'express'
import { authenticateAccessToken } from '../../middlewares'
import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
import { appStreamHtml } from './appStreamHtml'
const appStreams: { [key: string]: string } = {}
const router = express.Router()
router.get('/', authenticateAccessToken, async (req, res) => {
const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken())
return res.send(content)
})
export const publishAppStream = async (
appLoc: string,
streamWebFolder: string,
streamServiceName?: string,
streamLogo?: string,
addEntryToFile: boolean = true
) => {
const driveFilesPath = getFilesFolder()
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
const appLocPath = path.join(driveFilesPath, ...appLocParts)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
const pathToDeployment = path.join(appLocPath, 'services', streamWebFolder)
if (!pathToDeployment.includes(appLocPath)) {
throw new Error('streamWebFolder cannot be outside appLoc.')
}
if (await folderExists(pathToDeployment)) {
const appCount = process.appStreamConfig
? Object.keys(process.appStreamConfig).length
: 0
if (!streamServiceName) {
streamServiceName = `AppStreamName${appCount + 1}`
}
appStreams[streamServiceName] = pathToDeployment
addEntryToAppStreamConfig(
streamServiceName,
appLoc,
streamWebFolder,
streamLogo,
addEntryToFile
)
const sasJsPort = process.env.PORT || 5000
console.log(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)
return { streamServiceName }
}
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

Some files were not shown because too many files have changed in this diff Show More