mirror of
https://github.com/sasjs/server.git
synced 2026-01-14 17:30:05 +00:00
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -50,6 +50,7 @@ jobs:
|
|||||||
run: npm test
|
run: npm test
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
MODE: 'server'
|
||||||
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
||||||
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
||||||
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,5 @@ sas/
|
|||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
certificates/
|
certificates/
|
||||||
|
executables/
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
MODE=[server]
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
AUTH_CODE_SECRET=<secret>
|
AUTH_CODE_SECRET=<secret>
|
||||||
|
|||||||
1183
api/package-lock.json
generated
1183
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,21 @@
|
|||||||
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --coverage",
|
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --coverage",
|
||||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack"
|
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
||||||
|
"exe": "npm run build && npm run public:copy && npm run web:copy && pkg .",
|
||||||
|
"public:copy": "cp -r ./public/ ./build/public/",
|
||||||
|
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/"
|
||||||
|
},
|
||||||
|
"bin": "./build/src/server.js",
|
||||||
|
"pkg": {
|
||||||
|
"assets": [
|
||||||
|
"./build/public/**/*",
|
||||||
|
"./web/build/**/*"
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
"node16-macos-x64"
|
||||||
|
],
|
||||||
|
"outputPath": "../executables"
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"branches": [
|
"branches": [
|
||||||
@@ -23,8 +37,9 @@
|
|||||||
},
|
},
|
||||||
"author": "Analytium Ltd",
|
"author": "Analytium Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "^2.23.3",
|
"@sasjs/utils": "^2.33.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
@@ -37,6 +52,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
@@ -50,6 +66,7 @@
|
|||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "^8.0.0",
|
"mongodb-memory-server": "^8.0.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
|
"pkg": "^5.4.1",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.4.3",
|
"semantic-release": "^17.4.3",
|
||||||
|
|||||||
8933
api/public/SASjsApi/swagger-ui.css
Normal file
8933
api/public/SASjsApi/swagger-ui.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,93 @@ components:
|
|||||||
requestBodies: {}
|
requestBodies: {}
|
||||||
responses: {}
|
responses: {}
|
||||||
schemas:
|
schemas:
|
||||||
|
AuthorizeResponse:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Authorization code'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizePayload:
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: 'Username for user'
|
||||||
|
example: secretuser
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: 'Password for user'
|
||||||
|
example: secretpassword
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
description: 'Client ID'
|
||||||
|
example: clientID1
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
- clientId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
TokenResponse:
|
||||||
|
properties:
|
||||||
|
accessToken:
|
||||||
|
type: string
|
||||||
|
description: 'Access Token'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
refreshToken:
|
||||||
|
type: string
|
||||||
|
description: 'Refresh Token'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- accessToken
|
||||||
|
- refreshToken
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
TokenPayload:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
description: 'Client ID'
|
||||||
|
example: clientID1
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Authorization code'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- clientId
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
InfoJWT:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
userId:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
required:
|
||||||
|
- clientId
|
||||||
|
- userId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
ClientPayload:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
description: 'Client ID'
|
||||||
|
example: someFormattedClientID1234
|
||||||
|
clientSecret:
|
||||||
|
type: string
|
||||||
|
description: 'Client Secret'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- clientId
|
||||||
|
- clientSecret
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
MemberType.folder:
|
MemberType.folder:
|
||||||
enum:
|
enum:
|
||||||
- folder
|
- folder
|
||||||
@@ -151,28 +238,6 @@ components:
|
|||||||
- tree
|
- tree
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
ExecuteReturnJsonResponse:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
log:
|
|
||||||
type: string
|
|
||||||
result:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
ExecuteReturnJsonPayload:
|
|
||||||
properties:
|
|
||||||
_program:
|
|
||||||
type: string
|
|
||||||
description: 'Location of SAS program'
|
|
||||||
example: /Public/somefolder/some.file
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
UserResponse:
|
UserResponse:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@@ -293,91 +358,26 @@ components:
|
|||||||
- description
|
- description
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
ClientPayload:
|
ExecuteReturnJsonResponse:
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
status:
|
||||||
type: string
|
type: string
|
||||||
description: 'Client ID'
|
log:
|
||||||
example: someFormattedClientID1234
|
type: string
|
||||||
clientSecret:
|
result:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
type: string
|
type: string
|
||||||
description: 'Client Secret'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
required:
|
required:
|
||||||
- clientId
|
- status
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
AuthorizeResponse:
|
ExecuteReturnJsonPayload:
|
||||||
properties:
|
properties:
|
||||||
code:
|
_program:
|
||||||
type: string
|
type: string
|
||||||
description: 'Authorization code'
|
description: 'Location of SAS program'
|
||||||
example: someRandomCryptoString
|
example: /Public/somefolder/some.file
|
||||||
required:
|
|
||||||
- code
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizePayload:
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: 'Username for user'
|
|
||||||
example: secretuser
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
description: 'Password for user'
|
|
||||||
example: secretpassword
|
|
||||||
clientId:
|
|
||||||
type: string
|
|
||||||
description: 'Client ID'
|
|
||||||
example: clientID1
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
- password
|
|
||||||
- clientId
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
TokenResponse:
|
|
||||||
properties:
|
|
||||||
accessToken:
|
|
||||||
type: string
|
|
||||||
description: 'Access Token'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
refreshToken:
|
|
||||||
type: string
|
|
||||||
description: 'Refresh Token'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
required:
|
|
||||||
- accessToken
|
|
||||||
- refreshToken
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
TokenPayload:
|
|
||||||
properties:
|
|
||||||
clientId:
|
|
||||||
type: string
|
|
||||||
description: 'Client ID'
|
|
||||||
example: clientID1
|
|
||||||
code:
|
|
||||||
type: string
|
|
||||||
description: 'Authorization code'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- code
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
InfoJWT:
|
|
||||||
properties:
|
|
||||||
clientId:
|
|
||||||
type: string
|
|
||||||
userId:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- userId
|
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
@@ -393,6 +393,113 @@ info:
|
|||||||
name: 'Analytium Ltd'
|
name: 'Analytium Ltd'
|
||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
paths:
|
paths:
|
||||||
|
/SASjsApi/auth/authorize:
|
||||||
|
post:
|
||||||
|
operationId: Authorize
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizeResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {code: someRandomCryptoString}
|
||||||
|
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizePayload'
|
||||||
|
/SASjsApi/auth/token:
|
||||||
|
post:
|
||||||
|
operationId: Token
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString}
|
||||||
|
summary: 'Accepts client/auth code and returns access/refresh tokens'
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenPayload'
|
||||||
|
/SASjsApi/auth/refresh:
|
||||||
|
post:
|
||||||
|
operationId: Refresh
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString}
|
||||||
|
summary: 'Returns new access/refresh tokens'
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
/SASjsApi/auth/logout:
|
||||||
|
post:
|
||||||
|
operationId: Logout
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 'No content'
|
||||||
|
summary: 'Logout terminate access/refresh tokens and returns nothing'
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
/SASjsApi/client:
|
||||||
|
post:
|
||||||
|
operationId: CreateClient
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ClientPayload'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
|
||||||
|
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
|
||||||
|
tags:
|
||||||
|
- Client
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ClientPayload'
|
||||||
/SASjsApi/drive/deploy:
|
/SASjsApi/drive/deploy:
|
||||||
post:
|
post:
|
||||||
operationId: Deploy
|
operationId: Deploy
|
||||||
@@ -473,6 +580,40 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
|
post:
|
||||||
|
operationId: SaveFile
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UpdateFileResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: success}
|
||||||
|
'400':
|
||||||
|
description: 'File already exists'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UpdateFileResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: failure, message: 'File request failed.'}
|
||||||
|
summary: 'Create a file in SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FilePayload'
|
||||||
patch:
|
patch:
|
||||||
operationId: UpdateFile
|
operationId: UpdateFile
|
||||||
responses:
|
responses:
|
||||||
@@ -524,61 +665,6 @@ paths:
|
|||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters: []
|
||||||
/SASjsApi/client/execute:
|
|
||||||
get:
|
|
||||||
operationId: ExecuteReturnRaw
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
|
||||||
summary: 'Execute Stored Program, return raw content'
|
|
||||||
tags:
|
|
||||||
- STP
|
|
||||||
security:
|
|
||||||
-
|
|
||||||
bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
-
|
|
||||||
in: query
|
|
||||||
name: _program
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
example: /Public/somefolder/some.file
|
|
||||||
post:
|
|
||||||
operationId: ExecuteReturnJson
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
|
||||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
|
||||||
summary: 'Execute Stored Program, return JSON'
|
|
||||||
tags:
|
|
||||||
- STP
|
|
||||||
security:
|
|
||||||
-
|
|
||||||
bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
-
|
|
||||||
in: query
|
|
||||||
name: _program
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
example: /Public/somefolder/some.file
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
|
||||||
/SASjsApi/user:
|
/SASjsApi/user:
|
||||||
get:
|
get:
|
||||||
operationId: GetAllUsers
|
operationId: GetAllUsers
|
||||||
@@ -885,113 +971,61 @@ paths:
|
|||||||
format: double
|
format: double
|
||||||
type: number
|
type: number
|
||||||
example: '6789'
|
example: '6789'
|
||||||
/SASjsApi/client:
|
/SASjsApi/stp/execute:
|
||||||
post:
|
get:
|
||||||
operationId: CreateClient
|
operationId: ExecuteReturnRaw
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Ok
|
description: Ok
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ClientPayload'
|
type: string
|
||||||
examples:
|
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
||||||
'Example 1':
|
summary: 'Execute Stored Program, return raw content'
|
||||||
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
|
|
||||||
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
|
|
||||||
tags:
|
tags:
|
||||||
- Client
|
- STP
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters:
|
||||||
|
-
|
||||||
|
in: query
|
||||||
|
name: _program
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file
|
||||||
|
post:
|
||||||
|
operationId: ExecuteReturnJson
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
||||||
|
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
||||||
|
summary: 'Execute Stored Program, return JSON'
|
||||||
|
tags:
|
||||||
|
- STP
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
in: query
|
||||||
|
name: _program
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ClientPayload'
|
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||||
/SASjsApi/auth/authorize:
|
|
||||||
post:
|
|
||||||
operationId: Authorize
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizeResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {code: someRandomCryptoString}
|
|
||||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
|
||||||
tags:
|
|
||||||
- Auth
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizePayload'
|
|
||||||
/SASjsApi/auth/token:
|
|
||||||
post:
|
|
||||||
operationId: Token
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/TokenResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString}
|
|
||||||
summary: 'Accepts client/auth code and returns access/refresh tokens'
|
|
||||||
tags:
|
|
||||||
- Auth
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/TokenPayload'
|
|
||||||
/SASjsApi/auth/refresh:
|
|
||||||
post:
|
|
||||||
operationId: Refresh
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/TokenResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString}
|
|
||||||
summary: 'Returns new access/refresh tokens'
|
|
||||||
tags:
|
|
||||||
- Auth
|
|
||||||
security:
|
|
||||||
-
|
|
||||||
bearerAuth: []
|
|
||||||
parameters: []
|
|
||||||
/SASjsApi/auth/logout:
|
|
||||||
post:
|
|
||||||
operationId: Logout
|
|
||||||
responses:
|
|
||||||
'204':
|
|
||||||
description: 'No content'
|
|
||||||
summary: 'Logout terminate access/refresh tokens and returns nothing'
|
|
||||||
tags:
|
|
||||||
- Auth
|
|
||||||
security:
|
|
||||||
-
|
|
||||||
bearerAuth: []
|
|
||||||
parameters: []
|
|
||||||
servers:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
|
import path from 'path'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import morgan from 'morgan'
|
import morgan from 'morgan'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import cors from 'cors'
|
||||||
|
|
||||||
import webRouter from './routes/web'
|
import webRouter from './routes/web'
|
||||||
import apiRouter from './routes/api'
|
import apiRouter from './routes/api'
|
||||||
import { getWebBuildFolderPath } from './utils'
|
import { getWebBuildFolderPath } from './utils'
|
||||||
|
import { connectDB } from './routes/api/auth'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server') {
|
||||||
|
console.log('All CORS Requests are enabled')
|
||||||
|
app.use(cors({ credentials: true, origin: 'http://localhost:3000' }))
|
||||||
|
}
|
||||||
|
|
||||||
app.use(express.json({ limit: '50mb' }))
|
app.use(express.json({ limit: '50mb' }))
|
||||||
app.use(morgan('tiny'))
|
app.use(morgan('tiny'))
|
||||||
app.use(express.static('public'))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
app.use('/', webRouter)
|
app.use('/', webRouter)
|
||||||
app.use('/SASjsApi', apiRouter)
|
app.use('/SASjsApi', apiRouter)
|
||||||
@@ -16,4 +29,4 @@ app.use(express.json({ limit: '50mb' }))
|
|||||||
|
|
||||||
app.use(express.static(getWebBuildFolderPath()))
|
app.use(express.static(getWebBuildFolderPath()))
|
||||||
|
|
||||||
export default app
|
export default connectDB().then(() => app)
|
||||||
|
|||||||
@@ -106,6 +106,24 @@ export class DriveController {
|
|||||||
return getFile(filePath)
|
return getFile(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create a file in SASjs Drive
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<UpdateFileResponse>({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
@Response<UpdateFileResponse>(400, 'File already exists', {
|
||||||
|
status: 'failure',
|
||||||
|
message: 'File request failed.'
|
||||||
|
})
|
||||||
|
@Post('/file')
|
||||||
|
public async saveFile(
|
||||||
|
@Body() body: FilePayload
|
||||||
|
): Promise<UpdateFileResponse> {
|
||||||
|
return saveFile(body)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Modify a file in SASjs Drive
|
* @summary Modify a file in SASjs Drive
|
||||||
*
|
*
|
||||||
@@ -164,12 +182,35 @@ const getFile = async (filePath: string): Promise<GetFileResponse> => {
|
|||||||
const fileContent = await readFile(filePathFull)
|
const fileContent = await readFile(filePathFull)
|
||||||
|
|
||||||
return { status: 'success', fileContent: fileContent }
|
return { status: 'success', fileContent: fileContent }
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.',
|
message: 'File request failed.',
|
||||||
...(typeof err === 'object' ? err : { details: err })
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
||||||
|
const { filePath, fileContent } = body
|
||||||
|
try {
|
||||||
|
const filePathFull = path
|
||||||
|
.join(getTmpFilesFolderPath(), filePath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (await fileExists(filePathFull)) {
|
||||||
|
throw 'DriveController: File already exists.'
|
||||||
|
}
|
||||||
|
await createFile(filePathFull, fileContent)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
} catch (err: any) {
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'failure',
|
||||||
|
message: 'File request failed.',
|
||||||
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,12 +226,12 @@ const updateFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
|||||||
await createFile(filePathFull, fileContent)
|
await createFile(filePathFull, fileContent)
|
||||||
|
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.',
|
message: 'File request failed.',
|
||||||
...(typeof err === 'object' ? err : { details: err })
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,36 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { getSessionController } from './'
|
import { getSessionController } from './'
|
||||||
import { readFile, fileExists, createFile } from '@sasjs/utils'
|
import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils'
|
||||||
import { configuration } from '../../../package.json'
|
import { PreProgramVars, TreeNode } from '../../types'
|
||||||
import { promisify } from 'util'
|
|
||||||
import { execFile } from 'child_process'
|
|
||||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
|
||||||
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
|
||||||
|
|
||||||
export class ExecutionController {
|
export class ExecutionController {
|
||||||
async execute(
|
async execute(
|
||||||
program = '',
|
programPath: string,
|
||||||
preProgramVariables?: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
autoExec?: string,
|
vars: { [key: string]: string | number | undefined },
|
||||||
session?: Session,
|
|
||||||
vars?: any,
|
|
||||||
otherArgs?: any,
|
otherArgs?: any,
|
||||||
returnJson?: boolean
|
returnJson?: boolean
|
||||||
) {
|
) {
|
||||||
if (program) {
|
if (!(await fileExists(programPath)))
|
||||||
if (!(await fileExists(program))) {
|
throw 'ExecutionController: SAS file does not exist.'
|
||||||
throw 'ExecutionController: SAS file does not exist.'
|
|
||||||
}
|
|
||||||
|
|
||||||
program = await readFile(program)
|
let program = await readFile(programPath)
|
||||||
|
|
||||||
if (vars) {
|
Object.keys(vars).forEach(
|
||||||
Object.keys(vars).forEach(
|
(key: string) => (program = `%let ${key}=${vars[key]};\n${program}`)
|
||||||
(key: string) => (program = `%let ${key}=${vars[key]};\n${program}`)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
|
|
||||||
if (!session) {
|
const session = await sessionController.getSession()
|
||||||
session = await sessionController.getSession()
|
session.inUse = true
|
||||||
session.inUse = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let log = path.join(session.path, 'log.log')
|
const logPath = path.join(session.path, 'log.log')
|
||||||
|
|
||||||
let webout = path.join(session.path, 'webout.txt')
|
const weboutPath = path.join(session.path, 'webout.txt')
|
||||||
await createFile(webout, '')
|
await createFile(weboutPath, '')
|
||||||
|
|
||||||
const tokenFile = path.join(session.path, 'accessToken.txt')
|
const tokenFile = path.join(session.path, 'accessToken.txt')
|
||||||
await createFile(
|
await createFile(
|
||||||
@@ -60,7 +46,7 @@ export class ExecutionController {
|
|||||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||||
%let sasjsprocessmode=Stored Program;
|
%let sasjsprocessmode=Stored Program;
|
||||||
filename _webout "${webout}";
|
filename _webout "${weboutPath}";
|
||||||
${program}`
|
${program}`
|
||||||
|
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
@@ -76,53 +62,40 @@ ${program}`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = path.join(session.path, 'code.sas')
|
const codePath = path.join(session.path, 'code.sas')
|
||||||
if (!(await fileExists(code))) {
|
|
||||||
await createFile(code, program)
|
// 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 array
|
||||||
|
while (!session.completed || !session.crashed) {
|
||||||
|
await delay(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
let additionalArgs: string[] = []
|
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
||||||
if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec]
|
const webout = (await fileExists(weboutPath))
|
||||||
|
? await readFile(weboutPath)
|
||||||
|
: ''
|
||||||
|
|
||||||
const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
|
const debugValue =
|
||||||
'-SYSIN',
|
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
||||||
code,
|
|
||||||
'-LOG',
|
|
||||||
log,
|
|
||||||
'-WORK',
|
|
||||||
session.path,
|
|
||||||
...additionalArgs,
|
|
||||||
process.platform === 'win32' ? '-nosplash' : ''
|
|
||||||
]).catch((err) => ({ stderr: err, stdout: '' }))
|
|
||||||
|
|
||||||
if (await fileExists(log)) log = await readFile(log)
|
let debugResponse: string | undefined
|
||||||
else log = ''
|
if ((debugValue && debugValue >= 131) || session.crashed) {
|
||||||
|
debugResponse = `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||||
if (await fileExists(webout)) webout = await readFile(webout)
|
|
||||||
else webout = ''
|
|
||||||
|
|
||||||
const debug = Object.keys(vars).find(
|
|
||||||
(key: string) => key.toLowerCase() === '_debug'
|
|
||||||
)
|
|
||||||
|
|
||||||
let jsonResult
|
|
||||||
if ((debug && vars[debug] >= 131) || stderr) {
|
|
||||||
webout = `<html><body>
|
|
||||||
${webout}
|
|
||||||
<div style="text-align:left">
|
|
||||||
<hr /><h2>SAS Log</h2>
|
|
||||||
<pre>${log}</pre>
|
|
||||||
</div>
|
|
||||||
</body></html>`
|
|
||||||
} else if (returnJson) {
|
|
||||||
jsonResult = { result: webout, log: log }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
|
|
||||||
sessionController.deleteSession(session)
|
sessionController.deleteSession(session)
|
||||||
|
|
||||||
return Promise.resolve(jsonResult || webout)
|
if (returnJson) return { result: debugResponse ?? webout, log }
|
||||||
|
return debugResponse ?? webout
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDirectorytree() {
|
buildDirectorytree() {
|
||||||
@@ -162,3 +135,5 @@ ${webout}
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
|
import path from 'path'
|
||||||
import { Session } from '../../types'
|
import { Session } from '../../types'
|
||||||
|
import { configuration } from '../../../package.json'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { execFile } from 'child_process'
|
||||||
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../../utils'
|
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../../utils'
|
||||||
import {
|
import {
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
createFile,
|
createFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
deleteFile,
|
|
||||||
generateTimestamp
|
generateTimestamp
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import path from 'path'
|
|
||||||
import { ExecutionController } from './Execution'
|
const execFilePromise = promisify(execFile)
|
||||||
|
|
||||||
export class SessionController {
|
export class SessionController {
|
||||||
private sessions: Session[] = []
|
private sessions: Session[] = []
|
||||||
private executionController: ExecutionController
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.executionController = new ExecutionController()
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSession() {
|
public async getSession() {
|
||||||
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
|
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
|
||||||
@@ -32,74 +30,87 @@ export class SessionController {
|
|||||||
|
|
||||||
private async createSession() {
|
private async createSession() {
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
const sessionFolder = path.join(await getTmpSessionsFolderPath(), sessionId)
|
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||||
|
|
||||||
const autoExecContent = `data _null_;
|
|
||||||
/* remove the dummy SYSIN */
|
|
||||||
length fname $8;
|
|
||||||
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;
|
|
||||||
run;
|
|
||||||
EOL`
|
|
||||||
|
|
||||||
const autoExec = path.join(sessionFolder, 'autoexec.sas')
|
|
||||||
await createFile(autoExec, autoExecContent)
|
|
||||||
|
|
||||||
await createFile(path.join(sessionFolder, 'code.sas'), '')
|
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
const deathTimeStamp = (
|
||||||
|
parseInt(creationTimeStamp) +
|
||||||
|
15 * 60 * 1000 -
|
||||||
|
1000
|
||||||
|
).toString()
|
||||||
|
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
ready: false,
|
ready: false,
|
||||||
creationTimeStamp: creationTimeStamp,
|
inUse: false,
|
||||||
deathTimeStamp: (
|
completed: false,
|
||||||
parseInt(creationTimeStamp) +
|
creationTimeStamp,
|
||||||
15 * 60 * 1000 -
|
deathTimeStamp,
|
||||||
1000
|
path: sessionFolder
|
||||||
).toString(),
|
|
||||||
path: sessionFolder,
|
|
||||||
inUse: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we do not want to leave sessions running forever
|
||||||
|
// we clean them up after a predefined period, if unused
|
||||||
this.scheduleSessionDestroy(session)
|
this.scheduleSessionDestroy(session)
|
||||||
|
|
||||||
this.executionController
|
// the autoexec file is executed on SAS startup
|
||||||
.execute('', undefined, autoExec, session)
|
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||||
.catch(() => {})
|
await createFile(autoExecPath, autoExecContent)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const sasLoc = process.sasLoc ?? configuration.sasPath
|
||||||
|
execFilePromise(sasLoc, [
|
||||||
|
'-SYSIN',
|
||||||
|
codePath,
|
||||||
|
'-LOG',
|
||||||
|
path.join(session.path, 'log.log'),
|
||||||
|
'-WORK',
|
||||||
|
session.path,
|
||||||
|
'-AUTOEXEC',
|
||||||
|
autoExecPath,
|
||||||
|
process.platform === 'win32' ? '-nosplash' : ''
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
session.completed = true
|
||||||
|
console.log('session completed', session)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
session.completed = true
|
||||||
|
session.crashed = true
|
||||||
|
console.log('session crashed', session.id, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// we have a triggered session - add to array
|
||||||
this.sessions.push(session)
|
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)
|
await this.waitForSession(session)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitForSession(session: Session) {
|
public async waitForSession(session: Session) {
|
||||||
if (await fileExists(path.join(session.path, 'code.sas'))) {
|
const codeFilePath = path.join(session.path, 'code.sas')
|
||||||
while (await fileExists(path.join(session.path, 'code.sas'))) {}
|
|
||||||
|
|
||||||
await deleteFile(path.join(session.path, 'log.log'))
|
while (await fileExists(codeFilePath)) {}
|
||||||
|
|
||||||
session.ready = true
|
session.ready = true
|
||||||
|
return Promise.resolve(session)
|
||||||
return Promise.resolve(session)
|
|
||||||
} else {
|
|
||||||
session.ready = true
|
|
||||||
|
|
||||||
return Promise.resolve(session)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteSession(session: Session) {
|
public async deleteSession(session: Session) {
|
||||||
|
// remove the temporary files, to avoid buildup
|
||||||
await deleteFolder(session.path)
|
await deleteFolder(session.path)
|
||||||
|
|
||||||
|
// remove the session from the session array
|
||||||
if (session.ready) {
|
if (session.ready) {
|
||||||
this.sessions = this.sessions.filter(
|
this.sessions = this.sessions.filter(
|
||||||
(sess: Session) => sess.id !== session.id
|
(sess: Session) => sess.id !== session.id
|
||||||
@@ -127,3 +138,19 @@ export const getSessionController = (): SessionController => {
|
|||||||
|
|
||||||
return process.sessionController
|
return process.sessionController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autoExecContent = `
|
||||||
|
data _null_;
|
||||||
|
/* remove the dummy SYSIN */
|
||||||
|
length fname $8;
|
||||||
|
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;
|
||||||
|
`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import express from 'express'
|
import express, { response } from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import {
|
import {
|
||||||
Request,
|
Request,
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { ExecutionController } from './internal'
|
import { ExecutionController } from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
import { PreProgramVars } from '../types'
|
||||||
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
||||||
|
import { request } from 'https'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +31,7 @@ interface ExecuteReturnJsonResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/client')
|
@Route('SASjsApi/stp')
|
||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
export class STPController {
|
export class STPController {
|
||||||
/**
|
/**
|
||||||
@@ -75,6 +76,7 @@ const executeReturnRaw = async (
|
|||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string
|
_program: string
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
|
const query = req.query as { [key: string]: string | number | undefined }
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getTmpFilesFolderPath(), _program)
|
.join(getTmpFilesFolderPath(), _program)
|
||||||
@@ -84,20 +86,16 @@ const executeReturnRaw = async (
|
|||||||
const result = await new ExecutionController().execute(
|
const result = await new ExecutionController().execute(
|
||||||
sasCodePath,
|
sasCodePath,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
undefined,
|
query
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
...req.query
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return result as string
|
return result as string
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'Job execution failed.',
|
message: 'Job execution failed.',
|
||||||
...(typeof err === 'object' ? err : { details: err })
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,8 +115,6 @@ const executeReturnJson = async (
|
|||||||
const jsonResult: any = await new ExecutionController().execute(
|
const jsonResult: any = await new ExecutionController().execute(
|
||||||
sasCodePath,
|
sasCodePath,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
undefined,
|
|
||||||
req.sasSession,
|
|
||||||
{ ...req.query, ...req.body },
|
{ ...req.query, ...req.body },
|
||||||
{ filesNamesMap: filesNamesMap },
|
{ filesNamesMap: filesNamesMap },
|
||||||
true
|
true
|
||||||
@@ -128,11 +124,11 @@ const executeReturnJson = async (
|
|||||||
result: jsonResult.result,
|
result: jsonResult.result,
|
||||||
log: jsonResult.log
|
log: jsonResult.log
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'Job execution failed.',
|
message: 'Job execution failed.',
|
||||||
...(typeof err === 'object' ? err : { details: err })
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,20 @@ const authenticateToken = (
|
|||||||
key: string,
|
key: string,
|
||||||
tokenType: 'accessToken' | 'refreshToken' = 'accessToken'
|
tokenType: 'accessToken' | 'refreshToken' = 'accessToken'
|
||||||
) => {
|
) => {
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server') {
|
||||||
|
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 authHeader = req.headers['authorization']
|
||||||
const token = authHeader?.split(' ')[1]
|
const token = authHeader?.split(' ')[1]
|
||||||
if (!token) return res.sendStatus(401)
|
if (!token) return res.sendStatus(401)
|
||||||
|
|||||||
7
api/src/middlewares/desktopRestrict.ts
Normal file
7
api/src/middlewares/desktopRestrict.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const desktopRestrict = (req: any, res: any, next: any) => {
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server')
|
||||||
|
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './authenticateToken'
|
export * from './authenticateToken'
|
||||||
|
export * from './desktopRestrict'
|
||||||
export * from './verifyAdmin'
|
export * from './verifyAdmin'
|
||||||
export * from './verifyAdminIfNeeded'
|
export * from './verifyAdminIfNeeded'
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export const verifyAdmin = (req: any, res: any, next: any) => {
|
export const verifyAdmin = (req: any, res: any, next: any) => {
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server') return next()
|
||||||
|
|
||||||
const { user } = req
|
const { user } = req
|
||||||
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
||||||
next()
|
next()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from 'path'
|
|||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { configuration } from '../package.json'
|
import { configuration } from '../package.json'
|
||||||
import app from './app'
|
import appPromise from './app'
|
||||||
|
|
||||||
const keyPath = path.join('..', 'certificates', 'privkey.pem')
|
const keyPath = path.join('..', 'certificates', 'privkey.pem')
|
||||||
const certPath = path.join('..', 'certificates', 'fullchain.pem')
|
const certPath = path.join('..', 'certificates', 'fullchain.pem')
|
||||||
@@ -10,10 +10,12 @@ const certPath = path.join('..', 'certificates', 'fullchain.pem')
|
|||||||
const key = readFileSync(keyPath)
|
const key = readFileSync(keyPath)
|
||||||
const cert = readFileSync(certPath)
|
const cert = readFileSync(certPath)
|
||||||
|
|
||||||
const httpsServer = https.createServer({ key, cert }, app)
|
appPromise.then((app) => {
|
||||||
|
const httpsServer = https.createServer({ key, cert }, app)
|
||||||
|
|
||||||
httpsServer.listen(configuration.sasJsPort, () => {
|
httpsServer.listen(configuration.sasJsPort, () => {
|
||||||
console.log(
|
console.log(
|
||||||
`⚡️[server]: Server is running at https://localhost:${configuration.sasJsPort}`
|
`⚡️[server]: Server is running at https://localhost:${configuration.sasJsPort}`
|
||||||
)
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
authenticateRefreshToken
|
authenticateRefreshToken
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
import {
|
||||||
|
authorizeValidation,
|
||||||
|
getDesktopFields,
|
||||||
|
tokenValidation
|
||||||
|
} from '../../utils'
|
||||||
import { InfoJWT } from '../../types'
|
import { InfoJWT } from '../../types'
|
||||||
|
|
||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
@@ -24,7 +28,19 @@ export const populateClients = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const connectDB = () => {
|
export const connectDB = async () => {
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server') {
|
||||||
|
console.log('Running in Destop Mode, no DB to connect.')
|
||||||
|
|
||||||
|
const { sasLoc, driveLoc } = await getDesktopFields()
|
||||||
|
|
||||||
|
process.sasLoc = sasLoc
|
||||||
|
process.driveLoc = driveLoc
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 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') {
|
||||||
|
|||||||
@@ -35,6 +35,23 @@ driveRouter.get('/file', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
driveRouter.post('/file', async (req, res) => {
|
||||||
|
const { error, value: body } = updateFileDriveValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
const controller = new DriveController()
|
||||||
|
try {
|
||||||
|
const response = await controller.saveFile(body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
driveRouter.patch('/file', async (req, res) => {
|
driveRouter.patch('/file', async (req, res) => {
|
||||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
const { error, value: body } = updateFileDriveValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import dotenv from 'dotenv'
|
|
||||||
import swaggerUi from 'swagger-ui-express'
|
import swaggerUi from 'swagger-ui-express'
|
||||||
|
|
||||||
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
import {
|
||||||
|
authenticateAccessToken,
|
||||||
|
desktopRestrict,
|
||||||
|
verifyAdmin
|
||||||
|
} from '../../middlewares'
|
||||||
|
|
||||||
import driveRouter from './drive'
|
import driveRouter from './drive'
|
||||||
import stpRouter from './stp'
|
import stpRouter from './stp'
|
||||||
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, { connectDB } from './auth'
|
import authRouter from './auth'
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
connectDB()
|
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.use('/auth', authRouter)
|
router.use('/auth', desktopRestrict, authRouter)
|
||||||
router.use('/client', authenticateAccessToken, verifyAdmin, clientRouter)
|
router.use(
|
||||||
|
'/client',
|
||||||
|
desktopRestrict,
|
||||||
|
authenticateAccessToken,
|
||||||
|
verifyAdmin,
|
||||||
|
clientRouter
|
||||||
|
)
|
||||||
router.use('/drive', authenticateAccessToken, driveRouter)
|
router.use('/drive', authenticateAccessToken, driveRouter)
|
||||||
router.use('/group', groupRouter)
|
router.use('/group', desktopRestrict, groupRouter)
|
||||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||||
router.use('/user', userRouter)
|
router.use('/user', desktopRestrict, userRouter)
|
||||||
router.use(
|
router.use(
|
||||||
'/',
|
'/',
|
||||||
swaggerUi.serve,
|
swaggerUi.serve,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import app from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import {
|
import {
|
||||||
UserController,
|
UserController,
|
||||||
ClientController,
|
ClientController,
|
||||||
@@ -17,6 +18,11 @@ import {
|
|||||||
verifyTokenInDB
|
verifyTokenInDB
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
appPromise.then((_app) => {
|
||||||
|
app = _app
|
||||||
|
})
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const clientSecret = 'someclientSecret'
|
const clientSecret = 'someclientSecret'
|
||||||
const user = {
|
const user = {
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import app from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, ClientController } from '../../../controllers/'
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
appPromise.then((_app) => {
|
||||||
|
app = _app
|
||||||
|
})
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
clientId: 'someclientID',
|
clientId: 'someclientID',
|
||||||
clientSecret: 'someclientSecret'
|
clientSecret: 'someclientSecret'
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import app from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { getTmpFilesFolderPath } from '../../../utils/file'
|
import { getTmpFilesFolderPath } from '../../../utils/file'
|
||||||
@@ -10,6 +11,11 @@ import path from 'path'
|
|||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
import { FolderMember, ServiceMember } from '../../../types'
|
import { FolderMember, ServiceMember } from '../../../types'
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
appPromise.then((_app) => {
|
||||||
|
app = _app
|
||||||
|
})
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const user = {
|
const user = {
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import app from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, GroupController } from '../../../controllers/'
|
import { UserController, GroupController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
appPromise.then((_app) => {
|
||||||
|
app = _app
|
||||||
|
})
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import app from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
appPromise.then((_app) => {
|
||||||
|
app = _app
|
||||||
|
})
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { readFile } from '@sasjs/utils'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getWebBuildFolderPath } from '../../utils'
|
import { getWebBuildFolderPath } from '../../utils'
|
||||||
@@ -5,7 +6,24 @@ import { getWebBuildFolderPath } from '../../utils'
|
|||||||
const webRouter = express.Router()
|
const webRouter = express.Router()
|
||||||
|
|
||||||
webRouter.get('/', async (_, res) => {
|
webRouter.get('/', async (_, res) => {
|
||||||
res.sendFile(path.join(getWebBuildFolderPath(), 'index.html'))
|
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
||||||
|
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server') {
|
||||||
|
const content = await readFile(indexHtmlPath)
|
||||||
|
|
||||||
|
const codeToInject = `
|
||||||
|
<script>
|
||||||
|
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
||||||
|
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
||||||
|
</script>`
|
||||||
|
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html')
|
||||||
|
return res.send(injectedContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile(indexHtmlPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default webRouter
|
export default webRouter
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import app from './app'
|
import appPromise from './app'
|
||||||
import { configuration } from '../package.json'
|
import { configuration } from '../package.json'
|
||||||
|
|
||||||
app.listen(configuration.sasJsPort, () => {
|
appPromise.then((app) => {
|
||||||
console.log(
|
app.listen(configuration.sasJsPort, () => {
|
||||||
`⚡️[server]: Server is running at http://localhost:${configuration.sasJsPort}`
|
console.log(
|
||||||
)
|
`⚡️[server]: Server is running at http://localhost:${configuration.sasJsPort}`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
2
api/src/types/Process.d.ts
vendored
2
api/src/types/Process.d.ts
vendored
@@ -1,5 +1,7 @@
|
|||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
export interface Process {
|
export interface Process {
|
||||||
|
sasLoc?: string
|
||||||
|
driveLoc?: string
|
||||||
sessionController?: import('../controllers/internal').SessionController
|
sessionController?: import('../controllers/internal').SessionController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ export interface Session {
|
|||||||
deathTimeStamp: string
|
deathTimeStamp: string
|
||||||
path: string
|
path: string
|
||||||
inUse: boolean
|
inUse: boolean
|
||||||
|
completed: boolean
|
||||||
|
crashed?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const getWebBuildFolderPath = () =>
|
|||||||
getRealPath(path.join(__dirname, '..', '..', '..', 'web', 'build'))
|
getRealPath(path.join(__dirname, '..', '..', '..', 'web', 'build'))
|
||||||
|
|
||||||
export const getTmpFolderPath = () =>
|
export const getTmpFolderPath = () =>
|
||||||
getRealPath(path.join(__dirname, '..', '..', 'tmp'))
|
process.driveLoc ?? getRealPath(path.join(process.cwd(), 'tmp'))
|
||||||
|
|
||||||
export const getTmpFilesFolderPath = () =>
|
export const getTmpFilesFolderPath = () =>
|
||||||
path.join(getTmpFolderPath(), 'files')
|
path.join(getTmpFolderPath(), 'files')
|
||||||
|
|||||||
61
api/src/utils/getDesktopFields.ts
Normal file
61
api/src/utils/getDesktopFields.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { getString } from '@sasjs/utils/input'
|
||||||
|
import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
|
const isWindows = () => process.platform === 'win32'
|
||||||
|
|
||||||
|
export const getDesktopFields = async () => {
|
||||||
|
const sasLoc = await getSASLocation()
|
||||||
|
const driveLoc = await getDriveLocation()
|
||||||
|
|
||||||
|
return { sasLoc, driveLoc }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to files/drive is required.'
|
||||||
|
|
||||||
|
const drivePath = path.join(process.cwd(), filePath)
|
||||||
|
|
||||||
|
if (!(await folderExists(drivePath))) {
|
||||||
|
await createFolder(drivePath)
|
||||||
|
await createFolder(path.join(drivePath, 'files'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows() ? '.\\tmp\\' : './tmp/'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter path to file/drive (relative to executable): ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSASLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to SAS executable is required.'
|
||||||
|
|
||||||
|
if (!(await fileExists(filePath))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows()
|
||||||
|
? 'C:\\Program Files\\SASHome\\SASFoundation\\9.4\\sas.exe'
|
||||||
|
: '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter path to SAS executable (absolute path): ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ export * from './file'
|
|||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
|
export * from './getDesktopFields'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
export * from './sleep'
|
export * from './sleep'
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
|
|||||||
|
|
||||||
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()
|
||||||
})
|
})
|
||||||
.pattern(/\w\d/, Joi.string())
|
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
|
||||||
.validate(data)
|
.validate(data)
|
||||||
|
|||||||
14205
package-lock.json
generated
14205
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,21 +3,23 @@ import PropTypes from 'prop-types'
|
|||||||
|
|
||||||
import { CssBaseline, Box, TextField, Button } from '@mui/material'
|
import { CssBaseline, Box, TextField, Button } from '@mui/material'
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
const baseUrl =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'http://localhost:5000' : undefined
|
||||||
const getAuthCode = async (credentials: any) => {
|
const getAuthCode = async (credentials: any) => {
|
||||||
return fetch('/SASjsApi/auth/authorize', {
|
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(credentials)
|
body: JSON.stringify(credentials)
|
||||||
}).then((data) => data.json())
|
}).then((data) => data.json())
|
||||||
}
|
}
|
||||||
const getTokens = async (payload: any) => {
|
const getTokens = async (payload: any) => {
|
||||||
return fetch('/SASjsApi/auth/token', {
|
return fetch(`${baseUrl}/SASjsApi/auth/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
}).then((data) => data.json())
|
}).then((data) => data.json())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,15 +43,18 @@ export default function useTokens() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const baseUrl = 'http://localhost:5000'
|
const baseUrl =
|
||||||
// const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
process.env.NODE_ENV === 'development' ? 'http://localhost:5000' : undefined
|
||||||
|
|
||||||
|
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
||||||
|
|
||||||
const setAxiosRequestHeader = (accessToken: string) => {
|
const setAxiosRequestHeader = (accessToken: string) => {
|
||||||
axios.interceptors.request.use(function (config: any) {
|
axios.interceptors.request.use(function (config) {
|
||||||
// if (!isAbsoluteURLRegex.test(config.url)) {
|
if (baseUrl && !isAbsoluteURLRegex.test(config.url as string)) {
|
||||||
// config.url = baseUrl + config.url
|
config.url = baseUrl + config.url
|
||||||
// }
|
}
|
||||||
config.headers.Authorization = `Bearer ${accessToken}`
|
config.headers!['Authorization'] = `Bearer ${accessToken}`
|
||||||
|
config.withCredentials = true
|
||||||
|
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@@ -20,7 +16,5 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user