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
Yury Shkoda
bcef9a4a9d chore: wip replicating folders api 2022-06-21 18:28:13 +03:00
Yury Shkoda
a4d5ee99c4 fix(folders-api): add parent uri to root folder 2022-05-17 09:15:42 +03:00
Yury Shkoda
d7e835778b feat(folders-api): add root folder 2022-05-17 08:51:26 +03:00
Yury Shkoda
d7d3bb285f chore(deps): fixed security vulnerability 2022-04-19 17:00:25 +03:00
Yury Shkoda
d532d74879 docs(readme): fixed typos 2022-04-19 16:59:13 +03:00
44 changed files with 543 additions and 2192 deletions

View File

@@ -32,17 +32,10 @@ jobs:
env: env:
CI: true 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: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: | files: |
./executables/linux.zip ./executables/api-linux
./executables/macos.zip ./executables/api-macos
./executables/windows.zip ./executables/api-win.exe

View File

@@ -2,96 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.0.21](https://github.com/sasjs/server/compare/v0.0.20...v0.0.21) (2022-02-01)
### Bug Fixes
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
### [0.0.20](https://github.com/sasjs/server/compare/v0.0.2...v0.0.20) (2022-01-20)
### Bug Fixes
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
### [0.0.19](https://github.com/sasjs/server/compare/v0.0.18...v0.0.19) (2022-01-20)
### Bug Fixes
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
### [0.0.18](https://github.com/sasjs/server/compare/v0.0.17...v0.0.18) (2022-01-08)
### Bug Fixes
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
### [0.0.17](https://github.com/sasjs/server/compare/v0.0.16...v0.0.17) (2022-01-07)
### Bug Fixes
* bug removed, log is clean now ([43769e7](https://github.com/sasjs/server/commit/43769e711d37a4f670786545630139a2d926dc76))
### [0.0.16](https://github.com/sasjs/server/compare/v0.0.15...v0.0.16) (2022-01-07)
### Bug Fixes
* added sas9 server address ([cd83891](https://github.com/sasjs/server/commit/cd838915fdb216ee364ea677747409311b1214fb))
* recreate crashed session ([6cbc657](https://github.com/sasjs/server/commit/6cbc657da3eb7fa821a678443a3ae4079c2a1f09))
* session should be marked as consumed ([7a3d710](https://github.com/sasjs/server/commit/7a3d710153f37d12160ff45f8f97fb4fcc75d684))
### [0.0.15](https://github.com/sasjs/server/compare/v0.0.14...v0.0.15) (2022-01-06)
### Bug Fixes
* **studio:** web component updated ([2d77222](https://github.com/sasjs/server/commit/2d77222ae8a139acd9d96466d0e68291c4ebd70e))
* updated route for sas code ([e1eb044](https://github.com/sasjs/server/commit/e1eb04494a5650726c95990f74fc719eced4ccb5))
* **web:** autosave and autofocus ([51ee8c0](https://github.com/sasjs/server/commit/51ee8c0825f021d1d67b2d765d5b434cbf248a1f))
* **web:** parsing of webout ([a115160](https://github.com/sasjs/server/commit/a1151606f21e0007e2b1ca1245d592d96866f62a))
* **web:** sticky tabs on Studio + extra run code button removed ([450d99f](https://github.com/sasjs/server/commit/450d99f06e5929eb1679e6203284e4faa44e19b0))
### [0.0.14](https://github.com/sasjs/server/compare/v0.0.13...v0.0.14) (2021-12-19)
### Bug Fixes
* actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release ([d86c841](https://github.com/sasjs/server/commit/d86c841f1fb94455ac3500f215a42b4acb8b0017))
* bumping sasjs/core with adjustment to ms_webout() ([076b866](https://github.com/sasjs/server/commit/076b866c020fb017512c2764801022a57fe4cca8))
* switch to main branch ([ceca370](https://github.com/sasjs/server/commit/ceca370e2757baf2e8ebb90dab6dfd27f7b990fc))
### [0.0.13](https://github.com/sasjs/server/compare/v0.0.12...v0.0.13) (2021-12-16)
### Features
* **studio:** run selected code + open in studio ([27129a8](https://github.com/sasjs/server/commit/27129a8921084c72968383fdbc2ecbd2f417456c))
### Bug Fixes
* output for Studio ([e5be0e6](https://github.com/sasjs/server/commit/e5be0e678965b05c64bcc8f55c48a366e0ff55a3))
### [0.0.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
### Bug Fixes
* use env if provided for desktop mode ([d19ce25](https://github.com/sasjs/server/commit/d19ce253b4e2d2a7dd912d43a553d4c1bd60ba58))
### [0.0.11](https://github.com/sasjs/server/compare/v0.0.10...v0.0.11) (2021-12-15)
### Features
* added authorization route for web ([#37](https://github.com/sasjs/server/issues/37)) ([d0a1457](https://github.com/sasjs/server/commit/d0a1457f44a3d8993b57106e5e681c4e51fe8e7d))
### [0.0.10](https://github.com/sasjs/server/compare/v0.0.9...v0.0.10) (2021-12-07) ### [0.0.10](https://github.com/sasjs/server/compare/v0.0.9...v0.0.10) (2021-12-07)
### [0.0.9](https://github.com/sasjs/server/compare/v0.0.3...v0.0.9) (2021-12-07) ### [0.0.9](https://github.com/sasjs/server/compare/v0.0.3...v0.0.9) (2021-12-07)

View File

@@ -8,23 +8,6 @@ SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It
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). 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).
## Installation
First, download the relevant package from the [releases](https://github.com/sasjs/server/releases) page - either manually, or with commandline, eg as follow:
```bash
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
./api-linux
```
Second, trigger by double clicking (windows) or executing from commandline.
You are presented with two prompts:
* Location of your `sas.exe` / `sas.sh` executable
* Path to a filesystem location for Stored Programs and temporary files
## Configuration ## Configuration
Configuration is made in the `configuration` section of `package.json`: Configuration is made in the `configuration` section of `package.json`:
@@ -76,12 +59,12 @@ It will build following images if running first time:
### Using node: ### Using node:
#### Development (running api and web seperately): #### Development (running api and web separately):
##### API ##### API
Navigate to `./api` Navigate to `./api`
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files There is `.env.example` file present at `./api` directory. Remember to provide environment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server. Command to install and run api server.
``` ```

View File

@@ -5,7 +5,4 @@ PORT_WEB=[port for sasjs web component(react)] default value is 3000
ACCESS_TOKEN_SECRET=<secret> ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret> REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret> AUTH_CODE_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
DRIVE_PATH=./tmp

70
api/package-lock.json generated
View File

@@ -1,14 +1,14 @@
{ {
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.1",
"dependencies": { "dependencies": {
"@sasjs/core": "3.11.1", "@sasjs/core": "^2.48.6",
"@sasjs/utils": "2.34.1", "@sasjs/utils": "2.34.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -1551,9 +1551,9 @@
} }
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "3.11.1", "version": "2.48.6",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-3.11.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-2.48.6.tgz",
"integrity": "sha512-knS+FLBFy2TsDOSZy+8wNcydHXA/OIOgrc6Gcj3JfE3B4W+nkypGCMVAWywBrSUdeWlVpolMkFg12TKBfJvZ1A==" "integrity": "sha512-5kCf4TdCVOYve4wSHVTi+db34hknDwvY2C/JVEPHT6T3CkQ5cnwRVPSFz/1WzXzcVvdUi4ag5xd9SDOsU12oWA=="
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.34.1", "version": "2.34.1",
@@ -2607,9 +2607,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.6.0", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
"integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@@ -13408,9 +13408,9 @@
} }
}, },
"node_modules/source-map-support": { "node_modules/source-map-support": {
"version": "0.5.21", "version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
@@ -13918,16 +13918,16 @@
} }
}, },
"node_modules/swagger-ui-dist": { "node_modules/swagger-ui-dist": {
"version": "4.1.3", "version": "3.52.5",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz",
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" "integrity": "sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw=="
}, },
"node_modules/swagger-ui-express": { "node_modules/swagger-ui-express": {
"version": "4.2.0", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz",
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==", "integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==",
"dependencies": { "dependencies": {
"swagger-ui-dist": ">3.52.5" "swagger-ui-dist": "^3.18.1"
}, },
"engines": { "engines": {
"node": ">= v0.10.32" "node": ">= v0.10.32"
@@ -16102,9 +16102,9 @@
} }
}, },
"@sasjs/core": { "@sasjs/core": {
"version": "3.11.1", "version": "2.48.6",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-3.11.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-2.48.6.tgz",
"integrity": "sha512-knS+FLBFy2TsDOSZy+8wNcydHXA/OIOgrc6Gcj3JfE3B4W+nkypGCMVAWywBrSUdeWlVpolMkFg12TKBfJvZ1A==" "integrity": "sha512-5kCf4TdCVOYve4wSHVTi+db34hknDwvY2C/JVEPHT6T3CkQ5cnwRVPSFz/1WzXzcVvdUi4ag5xd9SDOsU12oWA=="
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.34.1", "version": "2.34.1",
@@ -17009,9 +17009,9 @@
} }
}, },
"acorn": { "acorn": {
"version": "8.6.0", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
"integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
"dev": true "dev": true
}, },
"acorn-globals": { "acorn-globals": {
@@ -25115,9 +25115,9 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}, },
"source-map-support": { "source-map-support": {
"version": "0.5.21", "version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true, "dev": true,
"requires": { "requires": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
@@ -25531,16 +25531,16 @@
} }
}, },
"swagger-ui-dist": { "swagger-ui-dist": {
"version": "4.1.3", "version": "3.52.5",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz",
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" "integrity": "sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw=="
}, },
"swagger-ui-express": { "swagger-ui-express": {
"version": "4.2.0", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz",
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==", "integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==",
"requires": { "requires": {
"swagger-ui-dist": ">3.52.5" "swagger-ui-dist": "^3.18.1"
} }
}, },
"symbol-tree": { "symbol-tree": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.1",
"description": "Api of SASjs server", "description": "Api of SASjs server",
"main": "./src/server.ts", "main": "./src/server.ts",
"scripts": { "scripts": {
@@ -41,12 +41,12 @@
}, },
"release": { "release": {
"branches": [ "branches": [
"main" "master"
] ]
}, },
"author": "4GL Ltd", "author": "Analytium Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "3.11.1", "@sasjs/core": "^2.48.6",
"@sasjs/utils": "2.34.1", "@sasjs/utils": "2.34.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -86,6 +86,6 @@
"typescript": "^4.3.2" "typescript": "^4.3.2"
}, },
"configuration": { "configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas" "sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4"
} }
} }

View File

@@ -92,16 +92,6 @@ components:
- clientSecret - clientSecret
type: object type: object
additionalProperties: false additionalProperties: false
ExecuteSASCodePayload:
properties:
code:
type: string
description: 'Code of SAS program'
example: '* SAS Code HERE;'
required:
- code
type: object
additionalProperties: false
MemberType.folder: MemberType.folder:
enum: enum:
- folder - folder
@@ -401,7 +391,7 @@ info:
version: 0.0.1 version: 0.0.1
description: 'Api of SASjs server' description: 'Api of SASjs server'
contact: contact:
name: '4GL Ltd' name: 'Analytium Ltd'
openapi: 3.0.0 openapi: 3.0.0
paths: paths:
/SASjsApi/auth/authorize: /SASjsApi/auth/authorize:
@@ -511,30 +501,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ClientPayload' $ref: '#/components/schemas/ClientPayload'
/SASjsApi/code/execute:
post:
operationId: ExecuteSASCode
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
description: 'Execute SAS code.'
summary: 'Run SAS Code and returns log'
tags:
- CODE
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExecuteSASCodePayload'
/SASjsApi/drive/deploy: /SASjsApi/drive/deploy:
post: post:
operationId: Deploy operationId: Deploy
@@ -1006,26 +972,6 @@ paths:
format: double format: double
type: number type: number
example: '6789' example: '6789'
/SASjsApi/session:
get:
operationId: Session
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
examples:
'Example 1':
value: {id: 123, username: johnusername, displayName: John}
summary: 'Get session info (username).'
tags:
- Session
security:
-
bearerAuth: []
parameters: []
/SASjsApi/stp/execute: /SASjsApi/stp/execute:
get: get:
operationId: ExecuteReturnRaw operationId: ExecuteReturnRaw
@@ -1081,6 +1027,26 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteReturnJsonPayload' $ref: '#/components/schemas/ExecuteReturnJsonPayload'
/SASjsApi/session:
get:
operationId: Session
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
examples:
'Example 1':
value: {id: 123, username: johnusername, displayName: John}
summary: 'Get session info (username).'
tags:
- Session
security:
-
bearerAuth: []
parameters: []
servers: servers:
- -
url: / url: /
@@ -1106,6 +1072,3 @@ tags:
- -
name: STP name: STP
description: 'Operations about STP' description: 'Operations about STP'
-
name: CODE
description: 'Operations on SAS code'

View File

@@ -8,19 +8,18 @@ import webRouter from './routes/web'
import apiRouter from './routes/api' import apiRouter from './routes/api'
import { connectDB, getWebBuildFolderPath } from './utils' import { connectDB, getWebBuildFolderPath } from './utils'
import { FolderController } from './controllers'
dotenv.config() dotenv.config()
const app = express() const app = express()
const { MODE, CORS, PORT_WEB } = process.env const { MODE, CORS, PORT_WEB } = process.env
const whiteList = [
`http://localhost:${PORT_WEB ?? 3000}`,
'https://sas.analytium.co.uk:8343'
]
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
console.log('All CORS Requests are enabled') console.log('All CORS Requests are enabled')
app.use(cors({ credentials: true, origin: whiteList })) app.use(
cors({ credentials: true, origin: `http://localhost:${PORT_WEB ?? 3000}` })
)
} }
app.use(express.json({ limit: '50mb' })) app.use(express.json({ limit: '50mb' }))
@@ -33,4 +32,8 @@ app.use(express.json({ limit: '50mb' }))
app.use(express.static(getWebBuildFolderPath())) app.use(express.static(getWebBuildFolderPath()))
const folderController = new FolderController()
folderController.addRootFolder()
export default connectDB().then(() => app) export default connectDB().then(() => app)

View File

@@ -1,63 +0,0 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController } from './internal'
import { PreProgramVars } from '../types'
interface ExecuteSASCodePayload {
/**
* Code of SAS program
* @example "* SAS Code HERE;"
*/
code: string
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
*/
@Post('/execute')
public async executeSASCode(
@Request() request: express.Request,
@Body() body: ExecuteSASCodePayload
): Promise<string> {
return executeSASCode(request, body)
}
}
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
try {
const result = await new ExecutionController().executeProgram(
code,
getPreProgramVariables(req),
{ ...req.query, _debug: 131 },
undefined,
true
)
return result as string
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const getPreProgramVariables = (req: any): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
serverUrl: protocol + host,
accessToken
}
}

View File

@@ -0,0 +1,69 @@
import { Body } from 'tsoa'
import Folder, { FolderPayload, MemberType } from '../model/Folder'
export class FolderController {
public async createFolder(@Body() body: FolderPayload) {
return createFolder(body)
}
public async addRootFolder() {
await addRootFolder()
}
}
interface FolderDetailsResponse {
name: string
parentFolderUri: string
children: []
}
const createFolder = async ({
name,
parentFolderUri,
type
}: FolderPayload): Promise<FolderDetailsResponse> => {
parentFolderUri = parentFolderUri.replace(/\/folders\/folders\//i, '')
const parentFolder = await Folder.findById(parentFolderUri).catch(
(_: any) => {
throw new Error(
`No folder with an URI '${parentFolderUri}' has been found.`
)
}
)
const folder = new Folder({
name,
parentFolderUri,
type
})
const savedFolder = await folder.save().catch((err: any) => {
// TODO: log error
throw new Error(`Error while saving folder.`)
})
await parentFolder?.addMember(savedFolder._id)
return {
name: savedFolder.name,
parentFolderUri: savedFolder.parentFolderUri,
children: []
}
}
const addRootFolder = async () => {
let folder = await Folder.findOne({ name: '/' })
if (folder) return
folder = new Folder({
name: '/',
parentFolderUri: '',
type: MemberType.Folder
})
folder.parentFolderUri = folder._id
return await folder.save()
}
const getItem = async({ path })

View File

@@ -1,8 +1,8 @@
export * from './auth' export * from './auth'
export * from './client' export * from './client'
export * from './code'
export * from './drive' export * from './drive'
export * from './group' export * from './group'
export * from './session'
export * from './stp' export * from './stp'
export * from './user' export * from './user'
export * from './session'
export * from './folder'

View File

@@ -6,7 +6,7 @@ import { PreProgramVars, TreeNode } from '../../types'
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils' import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
export class ExecutionController { export class ExecutionController {
async executeFile( async execute(
programPath: string, programPath: string,
preProgramVariables: PreProgramVars, preProgramVariables: PreProgramVars,
vars: { [key: string]: string | number | undefined }, vars: { [key: string]: string | number | undefined },
@@ -16,28 +16,12 @@ export class ExecutionController {
if (!(await fileExists(programPath))) if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.' throw 'ExecutionController: SAS file does not exist.'
const program = await readFile(programPath) let program = await readFile(programPath)
return this.executeProgram(
program,
preProgramVariables,
vars,
otherArgs,
returnJson
)
}
async executeProgram(
program: string,
preProgramVariables: PreProgramVars,
vars: { [key: string]: string | number | undefined },
otherArgs?: any,
returnJson?: boolean
) {
const sessionController = getSessionController() const sessionController = getSessionController()
const session = await sessionController.getSession() const session = await sessionController.getSession()
session.inUse = true session.inUse = true
session.consumed = true
const logPath = path.join(session.path, 'log.log') const logPath = path.join(session.path, 'log.log')
@@ -101,12 +85,14 @@ ${program}`
await createFile(codePath + '.bkp', program) await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath) await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status // we now need to poll the session array
while (!session.completed) { while (!session.completed) {
await delay(50) await delay(50)
} }
const log = (await fileExists(logPath)) ? await readFile(logPath) : '' const log =
((await fileExists(logPath)) ? await readFile(logPath) : '') +
session.crashed
const webout = (await fileExists(weboutPath)) const webout = (await fileExists(weboutPath))
? await readFile(weboutPath) ? await readFile(weboutPath)
: '' : ''
@@ -114,8 +100,8 @@ ${program}`
const debugValue = const debugValue =
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
// it should be deleted by scheduleSessionDestroy
session.inUse = false session.inUse = false
sessionController.deleteSession(session)
if (returnJson) { if (returnJson) {
return { return {

View File

@@ -12,8 +12,7 @@ import {
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile, readFile
moveFile
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -21,11 +20,8 @@ const execFilePromise = promisify(execFile)
export class SessionController { export class SessionController {
private sessions: Session[] = [] private sessions: Session[] = []
private getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
public async getSession() { public async getSession() {
const readySessions = this.getReadySessions() const readySessions = this.sessions.filter((sess: Session) => sess.ready)
const session = readySessions.length const session = readySessions.length
? readySessions[0] ? readySessions[0]
@@ -36,9 +32,8 @@ export class SessionController {
return session return session
} }
private async createSession(): Promise<Session> { private async createSession() {
const sessionId = generateUniqueFileName(generateTimestamp()) const sessionId = generateUniqueFileName(generateTimestamp())
console.log('creating session', sessionId)
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId) const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string const creationTimeStamp = sessionId.split('-').pop() as string
@@ -52,7 +47,6 @@ export class SessionController {
id: sessionId, id: sessionId,
ready: false, ready: false,
inUse: false, inUse: false,
consumed: false,
completed: false, completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
@@ -111,16 +105,15 @@ export class SessionController {
return session return session
} }
private async waitForSession(session: Session) { public async waitForSession(session: Session) {
const codeFilePath = path.join(session.path, 'code.sas') const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever // TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {} while ((await fileExists(codeFilePath)) && !session.crashed) {}
console.log('session crashed?', !!session.crashed, session.crashed)
if (session.crashed)
console.log('session crashed! while waiting to be ready', session.crashed)
session.ready = true session.ready = true
return Promise.resolve(session)
} }
public async deleteSession(session: Session) { public async deleteSession(session: Session) {
@@ -128,9 +121,11 @@ export class SessionController {
await deleteFolder(session.path) await deleteFolder(session.path)
// remove the session from the session array // remove the session from the session array
this.sessions = this.sessions.filter( if (session.ready) {
(sess: Session) => sess.id !== session.id this.sessions = this.sessions.filter(
) (sess: Session) => sess.id !== session.id
)
}
} }
private scheduleSessionDestroy(session: Session) { private scheduleSessionDestroy(session: Session) {
@@ -158,7 +153,6 @@ const autoExecContent = `
data _null_; data _null_;
/* remove the dummy SYSIN */ /* remove the dummy SYSIN */
length fname $8; length fname $8;
call missing(fname);
rc=filename(fname,getoption('SYSIN') ); rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname); if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname); rc=filename(fname);

View File

@@ -40,7 +40,6 @@ export class STPController {
): Promise<string> { ): Promise<string> {
return executeReturnRaw(request, _program) return executeReturnRaw(request, _program)
} }
/** /**
* Trigger a SAS program using it's location in the _program parameter. * Trigger a SAS program using it's location in the _program parameter.
* Enable debugging using the _debug parameter. * Enable debugging using the _debug parameter.
@@ -73,7 +72,7 @@ const executeReturnRaw = async (
.replace(new RegExp('/', 'g'), path.sep) + '.sas' .replace(new RegExp('/', 'g'), path.sep) + '.sas'
try { try {
const result = await new ExecutionController().executeFile( const result = await new ExecutionController().execute(
sasCodePath, sasCodePath,
getPreProgramVariables(req), getPreProgramVariables(req),
query query
@@ -102,7 +101,7 @@ const executeReturnJson = async (
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
try { try {
const { webout, log } = (await new ExecutionController().executeFile( const { webout, log } = (await new ExecutionController().execute(
sasCodePath, sasCodePath,
getPreProgramVariables(req), getPreProgramVariables(req),
{ ...req.query, ...req.body }, { ...req.query, ...req.body },

View File

@@ -1,5 +1,7 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { Request, Response } from 'express'
import { verifyTokenInDB } from '../utils' import { verifyTokenInDB } from '../utils'
import { headerIsNotPresentMessage, headerIsNotValidMessage } from './header'
export const authenticateAccessToken = (req: any, res: any, next: any) => { export const authenticateAccessToken = (req: any, res: any, next: any) => {
authenticateToken( authenticateToken(
@@ -21,6 +23,18 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
) )
} }
export const verifyAuthHeaderIsPresent = (req: Request, res: Response) => {
console.log(`🤖[verifyAuthHeaderIsPresent]🤖`)
const authHeader = req.headers.authorization
if (!authHeader) {
return res.status(401).json(headerIsNotPresentMessage('Authorization'))
} else if (!/^Bearer\s.{1}/.test(authHeader)) {
return res.status(401).json(headerIsNotValidMessage('Authorization'))
}
}
const authenticateToken = ( const authenticateToken = (
req: any, req: any,
res: any, res: any,

View File

@@ -0,0 +1,29 @@
import { Request, Response } from 'express'
export const verifyAcceptHeader = (req: Request, res: Response) => {
const acceptHeader = req.headers.accept
if (!acceptHeader) {
return res.status(406).json(headerIsNotPresentMessage('Accept'))
} else if (acceptHeader !== 'application/json') {
return res.status(406).json(headerIsNotValidMessage('Accept'))
}
}
export const verifyContentTypeHeader = (req: Request, res: Response) => {
const contentTypeHeader = req.headers['content-type']
if (!contentTypeHeader) {
return res.status(406).json(headerIsNotPresentMessage('Content-Type'))
} else if (contentTypeHeader !== 'application/json') {
return res.status(406).json(headerIsNotValidMessage('Content-Type'))
}
}
export const headerIsNotPresentMessage = (header: string) => ({
message: `${header} header is not present.`
})
export const headerIsNotValidMessage = (header: string) => ({
message: `${header} header is not valid.`
})

View File

@@ -2,3 +2,5 @@ export * from './authenticateToken'
export * from './desktop' export * from './desktop'
export * from './verifyAdmin' export * from './verifyAdmin'
export * from './verifyAdminIfNeeded' export * from './verifyAdminIfNeeded'
export * from './header'
export * from './mock'

View File

@@ -0,0 +1,24 @@
import {
verifyAuthHeaderIsPresent,
verifyAcceptHeader,
verifyContentTypeHeader
} from './'
import { Request, Response, NextFunction } from 'express'
export const verifyHeaders = (
req: Request,
res: Response,
next: NextFunction
) => {
switch (true) {
case verifyAuthHeaderIsPresent(req, res) !== undefined:
break
case verifyAcceptHeader(req, res) !== undefined:
break
case verifyContentTypeHeader(req, res) !== undefined:
break
default:
return next()
}
}

80
api/src/model/Folder.ts Normal file
View File

@@ -0,0 +1,80 @@
import { Document, Schema, Model, model } from 'mongoose'
import {} from '@sasjs/utils'
export interface FolderPayload {
parentFolderUri: string
name: string
type: MemberType
}
export enum MemberType {
Folder = 'Folder',
File = 'File'
}
const isMemberType = (value: string) => value in MemberType
export const getMemberType = (value: string) => {
value
}
interface IFolderDocument extends FolderPayload, Document {
members: Schema.Types.ObjectId[]
type: MemberType
}
interface IFolder extends IFolderDocument {
addMember(memberId: Schema.Types.ObjectId): Promise<IFolder>
}
interface IFolderModel extends Model<IFolder> {}
const folderSchema = new Schema({
name: { type: String, required: true },
parentFolderUri: { type: String, required: true },
members: [{ type: Schema.Types.ObjectId, refPath: 'member' }],
type: { type: String, required: true }
})
folderSchema.post('save', (folder: IFolder, next: Function) => {
folder.populate('members', '').then(() => next())
next()
})
// folderSchema.get('item', (folder: IFolder, next: Function) => {
// next()
// })
folderSchema.method(
'addMember',
async function (memberId: Schema.Types.ObjectId) {
const folderIdIndex = this.members.indexOf(memberId)
if (folderIdIndex === -1) this.members.push(memberId)
this.markModified('folders')
return this.save()
}
)
folderSchema.method('getItem', async function (path: string) {
console.log(`🤖[getItem]🤖`)
console.log(`🤖[path]🤖`, path)
// const folderIdIndex = this.members.indexOf(memberId)
// if (folderIdIndex === -1) this.members.push(memberId)
// this.markModified('folders')
// return this.save()
})
export const Folder: IFolderModel = model<IFolder, IFolderModel>(
'Folder',
folderSchema
)
export default Folder

View File

@@ -8,11 +8,7 @@ import {
authenticateRefreshToken authenticateRefreshToken
} from '../../middlewares' } from '../../middlewares'
import { import { authorizeValidation, tokenValidation } from '../../utils'
authorizeValidation,
getDesktopFields,
tokenValidation
} from '../../utils'
import { InfoJWT } from '../../types' import { InfoJWT } from '../../types'
const authRouter = express.Router() const authRouter = express.Router()

View File

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

View File

@@ -0,0 +1,67 @@
import express from 'express'
import { verifyHeaders } from '../../middlewares'
import { verifyQuery, setHeaders } from '../../utils'
import { FolderController } from '../../controllers'
const foldersRouter = express.Router()
const controller = new FolderController()
// https://sas.analytium.co.uk/folders/folders?parentFolderUri=/folders/folders/9e442a90-2c5b-40bb-982a-5fe3ff8a66b7
foldersRouter.post('/folders', verifyHeaders, async (req, res) => {
console.log(`🤖[req.query]🤖`, req.query)
console.log(`🤖[req.body]🤖`, req.body)
try {
const response = await controller.createFolder({
...req.query,
...req.body
})
console.log(`🤖[response]🤖`, response)
res.send(response)
} catch (err: any) {
console.log(`🤖[error]🤖`, err)
res.status(403).send(err.toString())
}
})
foldersRouter.get('/folders/@item', verifyHeaders, async (req, res) => {
const queryParam = 'path'
try {
const response = await controller.getItem({
...req.query,
...req.body
})
console.log(`🤖[response]🤖`, response)
res.send(response)
} catch (err: any) {
console.log(`🤖[error]🤖`, err)
res.status(403).send(err.toString())
}
// if (verifyQuery(req, res, [queryParam])) {
// const folderExist = Math.random() > 0.5
// setHeaders(res, folderExist)
// if (folderExist) {
// res.status(200).json({ message: 'Folder exists!' })
// } else {
// res.status(404).json({
// errorCode: 11512,
// message: 'No folders match the search criteria.',
// details: [`${queryParam}: ${req.query[queryParam]}`],
// links: [],
// version: 2
// })
// }
// }
})
export default foldersRouter

View File

@@ -11,12 +11,12 @@ import {
import driveRouter from './drive' import driveRouter from './drive'
import stpRouter from './stp' import stpRouter from './stp'
import codeRouter from './code'
import userRouter from './user' import userRouter from './user'
import groupRouter from './group' import groupRouter from './group'
import clientRouter from './client' import clientRouter from './client'
import authRouter from './auth' import authRouter from './auth'
import sessionRouter from './session' import sessionRouter from './session'
import foldersRouter from './folders'
const router = express.Router() const router = express.Router()
@@ -32,8 +32,8 @@ router.use(
router.use('/drive', authenticateAccessToken, driveRouter) router.use('/drive', authenticateAccessToken, driveRouter)
router.use('/group', desktopRestrict, groupRouter) router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter) router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter) router.use('/user', desktopRestrict, userRouter)
router.use('/folders', foldersRouter)
router.use( router.use(
'/', '/',
swaggerUi.serve, swaggerUi.serve,

View File

@@ -1,5 +1,5 @@
import express from 'express' import express from 'express'
import { executeProgramRawValidation, runSASValidation } from '../../utils' import { executeProgramRawValidation } from '../../utils'
import { STPController } from '../../controllers/' import { STPController } from '../../controllers/'
import { FileUploadController } from '../../controllers/internal' import { FileUploadController } from '../../controllers/internal'

View File

@@ -1,7 +1,7 @@
declare namespace NodeJS { declare namespace NodeJS {
export interface Process { export interface Process {
sasLoc: string sasLoc: string
driveLoc: string driveLoc?: string
sessionController?: import('../controllers/internal').SessionController sessionController?: import('../controllers/internal').SessionController
} }
} }

View File

@@ -5,7 +5,6 @@ export interface Session {
deathTimeStamp: string deathTimeStamp: string
path: string path: string
inUse: boolean inUse: boolean
consumed: boolean
completed: boolean completed: boolean
crashed?: string crashed?: string
} }

View File

@@ -3,17 +3,12 @@ import mongoose from 'mongoose'
import { configuration } from '../../package.json' import { configuration } from '../../package.json'
import { getDesktopFields } from '.' import { getDesktopFields } from '.'
import { populateClients } from '../routes/api/auth' import { populateClients } from '../routes/api/auth'
import { getRealPath } from '@sasjs/utils'
export const connectDB = async () => { export const connectDB = async () => {
// NOTE: when exporting app.js as agent for supertest // NOTE: when exporting app.js as agent for supertest
// we should exlcude connecting to the real database // we should exlcude connecting to the real database
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV !== 'test') {
process.driveLoc = path.join(process.cwd(), 'tmp')
return
} else {
const { MODE } = process.env const { MODE } = process.env
if (MODE?.trim() !== 'server') { if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.') console.log('Running in Destop Mode, no DB to connect.')
@@ -21,19 +16,16 @@ export const connectDB = async () => {
process.sasLoc = sasLoc process.sasLoc = sasLoc
process.driveLoc = driveLoc process.driveLoc = driveLoc
} else {
const { SAS_PATH, DRIVE_PATH } = process.env
process.sasLoc = SAS_PATH ?? configuration.sasPath return
process.driveLoc = getRealPath(
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
)
} }
console.log('sasLoc: ', process.sasLoc) const { SAS_PATH } = process.env
console.log('sasDrive: ', process.driveLoc) const sasDir = SAS_PATH ?? configuration.sasPath
if (MODE?.trim() !== 'server') return process.sasLoc = path.join(sasDir, 'sas')
console.log('sasLoc: ', process.sasLoc)
mongoose.connect(process.env.DB_CONNECT as string, async (err) => { mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err if (err) throw err

View File

@@ -1,4 +1,5 @@
import path from 'path' import path from 'path'
import { getRealPath } from '@sasjs/utils'
export const apiRoot = path.join(__dirname, '..', '..') export const apiRoot = path.join(__dirname, '..', '..')
export const codebaseRoot = path.join(apiRoot, '..') export const codebaseRoot = path.join(apiRoot, '..')
@@ -11,7 +12,8 @@ export const sysInitCompiledPath = path.join(
export const getWebBuildFolderPath = () => export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build') path.join(codebaseRoot, 'web', 'build')
export const getTmpFolderPath = () => process.driveLoc export const getTmpFolderPath = () =>
process.driveLoc ?? getRealPath(path.join(process.cwd(), 'tmp'))
export const getTmpFilesFolderPath = () => export const getTmpFilesFolderPath = () =>
path.join(getTmpFolderPath(), 'files') path.join(getTmpFolderPath(), 'files')

View File

@@ -5,10 +5,8 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32' const isWindows = () => process.platform === 'win32'
export const getDesktopFields = async () => { export const getDesktopFields = async () => {
const { SAS_PATH, DRIVE_PATH } = process.env const sasLoc = await getSASLocation()
const driveLoc = await getDriveLocation()
const sasLoc = SAS_PATH ?? (await getSASLocation())
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
return { sasLoc, driveLoc } return { sasLoc, driveLoc }
} }

View File

@@ -10,3 +10,4 @@ export * from './sleep'
export * from './upload' export * from './upload'
export * from './validation' export * from './validation'
export * from './verifyTokenInDB' export * from './verifyTokenInDB'
export * from './mock'

View File

@@ -0,0 +1,38 @@
import { Response } from 'express'
import { uuidv4 } from '@sasjs/utils'
export const setHeaders = (res: Response, isSuccess: boolean) => {
res.setHeader(
'cache-control',
`no-cache, no-store, max-age=0, must-revalidate`
)
res.setHeader(
'content-security-policy',
`default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' *.sas.com blob: data:; style-src 'self' 'unsafe-inline'; child-src 'self' blob: data: mailto:;`
)
res.setHeader(
'content-type',
`application/vnd.sas.${isSuccess ? 'content.folder' : 'error'}+json${
isSuccess ? '' : '; version=2;charset=UTF-8'
}`
)
res.setHeader('pragma', `no-cache`)
res.setHeader('server', `Apache/2.4`)
res.setHeader('strict-transport-security', `max-age=31536000`)
res.setHeader('Transfer-Encoding', `chunked`)
res.setHeader('vary', `User-Agent`)
res.setHeader('x-content-type-options', `nosniff`)
res.setHeader('x-frame-options', `SAMEORIGIN`)
res.setHeader('x-xss-protection', `1; mode=block`)
if (isSuccess) {
const uuid = uuidv4()
res.setHeader('content-location', `/folders/folders/${uuid}`)
res.setHeader('etag', `-2066812946`)
res.setHeader('last-modified', `${new Date(Date.now()).toUTCString()}`)
res.setHeader('location', `/folders/folders/${uuid}`)
} else {
res.setHeader('sas-service-response-flag', `true`)
}
}

View File

@@ -0,0 +1,2 @@
export * from './query'
export * from './header'

View File

@@ -0,0 +1,18 @@
import { Request, Response } from 'express'
export const verifyQuery = (req: Request, res: Response, args: string[]) => {
let isValid = true
const { query } = req
args.forEach((arg: string) => {
if (!Object.keys(query).includes(arg)) {
res.status(400).json({ message: `${arg} query argument is not present.` })
isValid = false
} else if (!query[arg]) {
res.status(400).json({ message: `${arg} query argument is not valid.` })
isValid = false
}
})
return isValid
}

View File

@@ -77,11 +77,6 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
fileContent: Joi.string().required() fileContent: Joi.string().required()
}).validate(data) }).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required()
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult => export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
_program: Joi.string().required() _program: Joi.string().required()

View File

@@ -38,10 +38,6 @@
{ {
"name": "STP", "name": "STP",
"description": "Operations about STP" "description": "Operations about STP"
},
{
"name": "CODE",
"description": "Operations on SAS code"
} }
], ],
"yaml": true, "yaml": true,

View File

@@ -7,7 +7,7 @@ services:
context: . context: .
dockerfile: DockerfileApi dockerfile: DockerfileApi
environment: environment:
MODE: 'server' MODE: ${MODE}
CORS: ${CORS} CORS: ${CORS}
PORT: ${PORT_API} PORT: ${PORT_API}
PORT_WEB: ${PORT_WEB} PORT_WEB: ${PORT_WEB}
@@ -43,7 +43,7 @@ services:
- ./web:/usr/server/web - ./web:/usr/server/web
mongodb: mongodb:
image: mongo:5.0.4 image: mongo:latest
ports: ports:
- 27017:27017 - 27017:27017
volumes: volumes:

1587
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
{ {
"name": "server", "name": "server",
"version": "0.0.21", "version": "0.0.10",
"description": "NodeJS wrapper for calling the SAS binary executable", "description": "NodeJS wrapper for calling the SAS binary executable",
"repository": "https://github.com/sasjs/server",
"scripts": { "scripts": {
"server": "npm run server:prepare && npm run server:start", "server": "npm run server:prepare && npm run server:start",
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..", "server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",

View File

@@ -19,14 +19,7 @@ function App() {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<HashRouter> <HashRouter>
<Header /> <Header />
<Switch> <Login setTokens={setTokens} />
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
<Route path="/">
<Login setTokens={setTokens} />
</Route>
</Switch>
</HashRouter> </HashRouter>
</ThemeProvider> </ThemeProvider>
) )
@@ -46,9 +39,6 @@ function App() {
<Route exact path="/SASjsStudio"> <Route exact path="/SASjsStudio">
<Studio /> <Studio />
</Route> </Route>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
</Switch> </Switch>
</HashRouter> </HashRouter>
</ThemeProvider> </ThemeProvider>

View File

@@ -23,7 +23,7 @@ const Home = () => {
and contributions are welcomed. and contributions are welcomed.
</p> </p>
<p> <p>
SASjs Server is maintained by the SAS Apps team -{' '} SASjs Server is maintained by the SASjs Apps team -{' '}
<a <a
href="https://sasapps.io/contact-us" href="https://sasapps.io/contact-us"
target="_blank" target="_blank"

View File

@@ -1,8 +1,7 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useLocation } from 'react-router-dom'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material' import { CssBaseline, Box, TextField, Button } from '@mui/material'
const headers = { const headers = {
Accept: 'application/json', Accept: 'application/json',
@@ -19,12 +18,7 @@ const getAuthCode = async (credentials: any) => {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify(credentials) body: JSON.stringify(credentials)
}).then(async (response) => { }).then((data) => data.json())
const resText = await response.text()
if (response.status !== 200) throw resText
return JSON.parse(resText)
})
} }
const getTokens = async (payload: any) => { const getTokens = async (payload: any) => {
return fetch(`${baseUrl}/SASjsApi/auth/token`, { return fetch(`${baseUrl}/SASjsApi/auth/token`, {
@@ -34,62 +28,26 @@ const getTokens = async (payload: any) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const Login = ({ setTokens, getCodeOnly }: any) => { const Login = ({ setTokens }: any) => {
const location = useLocation() const [username, setUserName] = useState()
const [username, setUserName] = useState('') const [password, setPassword] = useState()
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('')
let error: boolean
const [displayCode, setDisplayCode] = useState(null)
const handleSubmit = async (e: any) => { const handleSubmit = async (e: any) => {
error = false
setErrorMessage('')
e.preventDefault() e.preventDefault()
let { REACT_APP_CLIENT_ID: clientId } = process.env const { REACT_APP_CLIENT_ID: clientId } = process.env
if (getCodeOnly) {
const params = new URLSearchParams(location.search)
const responseType = params.get('response_type')
if (responseType === 'code')
clientId = params.get('client_id') ?? undefined
}
const { code } = await getAuthCode({ const { code } = await getAuthCode({
clientId, clientId,
username, username,
password password
}).catch((err: string) => {
error = true
setErrorMessage(err)
return {}
}) })
if (!error) { const { accessToken, refreshToken } = await getTokens({
if (getCodeOnly) return setDisplayCode(code) clientId,
code
})
const { accessToken, refreshToken } = await getTokens({ setTokens(accessToken, refreshToken)
clientId,
code
})
setTokens(accessToken, refreshToken)
}
}
if (displayCode) {
return (
<Box className="main">
<CssBaseline />
<br />
<h2>Authorization Code</h2>
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
{displayCode}
</Typography>
<br />
</Box>
)
} }
return ( return (
@@ -103,12 +61,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
> >
<CssBaseline /> <CssBaseline />
<br /> <br />
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2> <h2>Welcome to SASjs Server!</h2>
{getCodeOnly && (
<p style={{ width: 'auto' }}>
Provide credentials to get authorization code.
</p>
)}
<br /> <br />
<TextField <TextField
@@ -127,7 +80,6 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
onChange={(e: any) => setPassword(e.target.value)} onChange={(e: any) => setPassword(e.target.value)}
required required
/> />
{errorMessage && <span>{errorMessage}</span>}
<Button type="submit" variant="outlined"> <Button type="submit" variant="outlined">
Submit Submit
</Button> </Button>
@@ -136,8 +88,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
} }
Login.propTypes = { Login.propTypes = {
setTokens: PropTypes.func, setTokens: PropTypes.func.isRequired
getCodeOnly: PropTypes.bool
} }
export default Login export default Login

View File

@@ -3,8 +3,15 @@ import { useEffect, useState } from 'react'
export default function useTokens() { export default function useTokens() {
const getTokens = () => { const getTokens = () => {
const accessToken = localStorage.getItem('accessToken') const accessTokenString = localStorage.getItem('accessToken')
const refreshToken = localStorage.getItem('refreshToken') const accessToken: string = accessTokenString
? JSON.parse(accessTokenString)
: undefined
const refreshTokenString = localStorage.getItem('refreshToken')
const refreshToken: string = refreshTokenString
? JSON.parse(refreshTokenString)
: undefined
if (accessToken && refreshToken) { if (accessToken && refreshToken) {
setAxiosRequestHeader(accessToken) setAxiosRequestHeader(accessToken)
@@ -24,8 +31,8 @@ export default function useTokens() {
setAxiosResponse(setTokens) setAxiosResponse(setTokens)
const saveTokens = (accessToken: string, refreshToken: string) => { const saveTokens = (accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', accessToken) localStorage.setItem('accessToken', JSON.stringify(accessToken))
localStorage.setItem('refreshToken', refreshToken) localStorage.setItem('refreshToken', JSON.stringify(refreshToken))
setAxiosRequestHeader(accessToken) setAxiosRequestHeader(accessToken)
setTokens({ accessToken, refreshToken }) setTokens({ accessToken, refreshToken })
} }

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios' import axios from 'axios'
import Editor from '@monaco-editor/react' import Editor from '@monaco-editor/react'
@@ -90,10 +89,10 @@ const Main = (props: any) => {
style={{ position: 'absolute', left: '50%', top: '50%' }} style={{ position: 'absolute', left: '50%', top: '50%' }}
/> />
)} )}
{!isLoading && props?.selectedFilePath && !editMode && ( {!isLoading && props?.selectedFilePath !== '' && !editMode && (
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code> <code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
)} )}
{!isLoading && props?.selectedFilePath && editMode && ( {!isLoading && props?.selectedFilePath !== '' && editMode && (
<Editor <Editor
height="95%" height="95%"
value={fileContent} value={fileContent}
@@ -111,26 +110,17 @@ const Main = (props: any) => {
<Button <Button
variant="contained" variant="contained"
onClick={handleEditSaveBtnClick} onClick={handleEditSaveBtnClick}
disabled={isLoading || !props?.selectedFilePath} disabled={isLoading || props?.selectedFilePath === ''}
> >
{!editMode ? 'Edit' : 'Save'} {!editMode ? 'Edit' : 'Save'}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
onClick={handleCancelExecuteBtnClick} onClick={handleCancelExecuteBtnClick}
disabled={isLoading || !props?.selectedFilePath} disabled={isLoading || props?.selectedFilePath === ''}
> >
{editMode ? 'Cancel' : 'Execute'} {editMode ? 'Cancel' : 'Execute'}
</Button> </Button>
{props?.selectedFilePath && (
<Button
variant="contained"
component={Link}
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
>
Open in Studio
</Button>
)}
</Stack> </Stack>
</Box> </Box>
) )

View File

@@ -1,165 +1,14 @@
import React, { useEffect, useRef, useState } from 'react' import React from 'react'
import axios from 'axios'
import CssBaseline from '@mui/material/CssBaseline'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import { Button, Paper, Stack, Tab } from '@mui/material'
import { makeStyles } from '@mui/styles'
import Editor, { OnMount } from '@monaco-editor/react'
import { useLocation } from 'react-router-dom'
import { TabContext, TabList, TabPanel } from '@mui/lab'
const useStyles = makeStyles(() => ({
root: {
fontSize: '1rem',
color: 'gray',
'&.Mui-selected': {
color: 'black'
}
}
}))
const Studio = () => { const Studio = () => {
const location = useLocation()
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const [webout, setWebout] = useState('')
const [tab, setTab] = React.useState('1')
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
const editorRef = useRef(null as any)
const handleEditorDidMount: OnMount = (editor) => {
editor.focus()
editorRef.current = editor
}
const getSelection = () => {
const editor = editorRef.current as any
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
return selection ?? ''
}
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
const runCode = (code: string) => {
axios
.post(`/SASjsApi/code/execute`, { code })
.then((res: any) => {
setLog(`<div><h2>SAS Log</h2><pre>${res?.data?.log}</pre></div>`)
let weboutString: string
try {
weboutString = res.data.webout
.split('>>weboutBEGIN<<')[1]
.split('>>weboutEND<<')[0]
} catch (_) {
weboutString = res?.data?.webout ?? ''
}
let webout: string
try {
webout = JSON.stringify(JSON.parse(weboutString), null, 4)
} catch (_) {
webout = weboutString
}
setWebout(`<pre><code>${webout}</code></pre>`)
setTab('2')
})
.catch((err) => console.log(err))
}
useEffect(() => {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
}, [])
useEffect(() => {
if (fileContent.length) {
localStorage.setItem('fileContent', fileContent)
}
}, [fileContent])
useEffect(() => {
const params = new URLSearchParams(location.search)
const programPath = params.get('_program')
if (programPath?.length)
axios
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
.then((res: any) => setFileContent(res.data.fileContent))
.catch((err) => console.log(err))
}, [location.search])
const classes = useStyles()
return ( return (
<> <Box className="main">
<br /> <CssBaseline />
<br /> <h2>This is container for SASjs studio</h2>
<br /> </Box>
<Box sx={{ width: '100%', typography: 'body1' }}>
<TabContext value={tab}>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider'
}}
style={{ position: 'fixed', background: 'white', width: '100%' }}
>
<TabList onChange={handleTabChange} centered>
<Tab className={classes.root} label="Code" value="1" />
<Tab className={classes.root} label="Log" value="2" />
<Tab className={classes.root} label="Webout" value="3" />
</TabList>
</Box>
<TabPanel value="1">
{/* <Toolbar /> */}
<Paper
sx={{
height: '70vh',
marginTop: '50px',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
<Editor
height="95%"
value={fileContent}
onMount={handleEditorDidMount}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
</Paper>
<Stack
spacing={3}
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button variant="contained" onClick={handleRunBtnClick}>
Run SAS Code
</Button>
</Stack>
</TabPanel>
<TabPanel value="2">
<div
id="sas_log"
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: log }}
/>
</TabPanel>
<TabPanel value="3">
<div
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: webout }}
/>
</TabPanel>
</TabContext>
</Box>
</>
) )
} }