mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -50,6 +50,7 @@ jobs:
|
||||
run: npm test
|
||||
env:
|
||||
CI: true
|
||||
MODE: 'server'
|
||||
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
||||
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
||||
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,5 @@ sas/
|
||||
tmp/
|
||||
build/
|
||||
certificates/
|
||||
executables/
|
||||
.env
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
MODE=[server]
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_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",
|
||||
"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}\"",
|
||||
"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": {
|
||||
"branches": [
|
||||
@@ -23,8 +37,9 @@
|
||||
},
|
||||
"author": "Analytium Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.23.3",
|
||||
"@sasjs/utils": "^2.33.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
@@ -37,6 +52,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
@@ -50,6 +66,7 @@
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "^8.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"pkg": "^5.4.1",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"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: {}
|
||||
responses: {}
|
||||
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:
|
||||
enum:
|
||||
- folder
|
||||
@@ -151,28 +238,6 @@ components:
|
||||
- tree
|
||||
type: object
|
||||
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:
|
||||
properties:
|
||||
id:
|
||||
@@ -293,91 +358,26 @@ components:
|
||||
- description
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ClientPayload:
|
||||
ExecuteReturnJsonResponse:
|
||||
properties:
|
||||
clientId:
|
||||
status:
|
||||
type: string
|
||||
description: 'Client ID'
|
||||
example: someFormattedClientID1234
|
||||
clientSecret:
|
||||
log:
|
||||
type: string
|
||||
result:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
description: 'Client Secret'
|
||||
example: someRandomCryptoString
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
- status
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizeResponse:
|
||||
ExecuteReturnJsonPayload:
|
||||
properties:
|
||||
code:
|
||||
_program:
|
||||
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
|
||||
description: 'Location of SAS program'
|
||||
example: /Public/somefolder/some.file
|
||||
type: object
|
||||
additionalProperties: false
|
||||
securitySchemes:
|
||||
@@ -393,6 +393,113 @@ info:
|
||||
name: 'Analytium Ltd'
|
||||
openapi: 3.0.0
|
||||
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:
|
||||
post:
|
||||
operationId: Deploy
|
||||
@@ -473,6 +580,40 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
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:
|
||||
operationId: UpdateFile
|
||||
responses:
|
||||
@@ -524,61 +665,6 @@ paths:
|
||||
-
|
||||
bearerAuth: []
|
||||
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:
|
||||
get:
|
||||
operationId: GetAllUsers
|
||||
@@ -885,113 +971,61 @@ paths:
|
||||
format: double
|
||||
type: number
|
||||
example: '6789'
|
||||
/SASjsApi/client:
|
||||
post:
|
||||
operationId: CreateClient
|
||||
/SASjsApi/stp/execute:
|
||||
get:
|
||||
operationId: ExecuteReturnRaw
|
||||
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.'
|
||||
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:
|
||||
- Client
|
||||
- STP
|
||||
security:
|
||||
-
|
||||
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:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ClientPayload'
|
||||
/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: []
|
||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||
servers:
|
||||
-
|
||||
url: /
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import morgan from 'morgan'
|
||||
import dotenv from 'dotenv'
|
||||
import cors from 'cors'
|
||||
|
||||
import webRouter from './routes/web'
|
||||
import apiRouter from './routes/api'
|
||||
import { getWebBuildFolderPath } from './utils'
|
||||
import { connectDB } from './routes/api/auth'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
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(morgan('tiny'))
|
||||
app.use(express.static('public'))
|
||||
app.use(express.static(path.join(__dirname, '../public')))
|
||||
|
||||
app.use('/', webRouter)
|
||||
app.use('/SASjsApi', apiRouter)
|
||||
@@ -16,4 +29,4 @@ app.use(express.json({ limit: '50mb' }))
|
||||
|
||||
app.use(express.static(getWebBuildFolderPath()))
|
||||
|
||||
export default app
|
||||
export default connectDB().then(() => app)
|
||||
|
||||
@@ -106,6 +106,24 @@ export class DriveController {
|
||||
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
|
||||
*
|
||||
@@ -164,12 +182,35 @@ const getFile = async (filePath: string): Promise<GetFileResponse> => {
|
||||
const fileContent = await readFile(filePathFull)
|
||||
|
||||
return { status: 'success', fileContent: fileContent }
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
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)
|
||||
|
||||
return { status: 'success' }
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
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 fs from 'fs'
|
||||
import { getSessionController } from './'
|
||||
import { readFile, fileExists, createFile } from '@sasjs/utils'
|
||||
import { configuration } from '../../../package.json'
|
||||
import { promisify } from 'util'
|
||||
import { execFile } from 'child_process'
|
||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||
import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils'
|
||||
import { PreProgramVars, TreeNode } from '../../types'
|
||||
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
export class ExecutionController {
|
||||
async execute(
|
||||
program = '',
|
||||
preProgramVariables?: PreProgramVars,
|
||||
autoExec?: string,
|
||||
session?: Session,
|
||||
vars?: any,
|
||||
programPath: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: { [key: string]: string | number | undefined },
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
) {
|
||||
if (program) {
|
||||
if (!(await fileExists(program))) {
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
}
|
||||
if (!(await fileExists(programPath)))
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
|
||||
program = await readFile(program)
|
||||
let program = await readFile(programPath)
|
||||
|
||||
if (vars) {
|
||||
Object.keys(vars).forEach(
|
||||
(key: string) => (program = `%let ${key}=${vars[key]};\n${program}`)
|
||||
)
|
||||
}
|
||||
}
|
||||
Object.keys(vars).forEach(
|
||||
(key: string) => (program = `%let ${key}=${vars[key]};\n${program}`)
|
||||
)
|
||||
|
||||
const sessionController = getSessionController()
|
||||
|
||||
if (!session) {
|
||||
session = await sessionController.getSession()
|
||||
session.inUse = true
|
||||
}
|
||||
const session = await sessionController.getSession()
|
||||
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')
|
||||
await createFile(webout, '')
|
||||
const weboutPath = path.join(session.path, 'webout.txt')
|
||||
await createFile(weboutPath, '')
|
||||
|
||||
const tokenFile = path.join(session.path, 'accessToken.txt')
|
||||
await createFile(
|
||||
@@ -60,7 +46,7 @@ export class ExecutionController {
|
||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||
%let sasjsprocessmode=Stored Program;
|
||||
filename _webout "${webout}";
|
||||
filename _webout "${weboutPath}";
|
||||
${program}`
|
||||
|
||||
// if no files are uploaded filesNamesMap will be undefined
|
||||
@@ -76,53 +62,40 @@ ${program}`
|
||||
}
|
||||
}
|
||||
|
||||
const code = path.join(session.path, 'code.sas')
|
||||
if (!(await fileExists(code))) {
|
||||
await createFile(code, program)
|
||||
const codePath = path.join(session.path, 'code.sas')
|
||||
|
||||
// Creating this file in a RUNNING session will break out
|
||||
// the autoexec loop and actually execute the program
|
||||
// but - given it will take several milliseconds to create
|
||||
// (which can mean SAS trying to run a partial program, or
|
||||
// failing due to file lock) we first create the file THEN
|
||||
// we rename it.
|
||||
await createFile(codePath + '.bkp', program)
|
||||
await moveFile(codePath + '.bkp', codePath)
|
||||
|
||||
// we now need to poll the session array
|
||||
while (!session.completed || !session.crashed) {
|
||||
await delay(50)
|
||||
}
|
||||
|
||||
let additionalArgs: string[] = []
|
||||
if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec]
|
||||
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? await readFile(weboutPath)
|
||||
: ''
|
||||
|
||||
const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
|
||||
'-SYSIN',
|
||||
code,
|
||||
'-LOG',
|
||||
log,
|
||||
'-WORK',
|
||||
session.path,
|
||||
...additionalArgs,
|
||||
process.platform === 'win32' ? '-nosplash' : ''
|
||||
]).catch((err) => ({ stderr: err, stdout: '' }))
|
||||
const debugValue =
|
||||
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
||||
|
||||
if (await fileExists(log)) log = await readFile(log)
|
||||
else log = ''
|
||||
|
||||
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 }
|
||||
let debugResponse: string | undefined
|
||||
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>`
|
||||
}
|
||||
|
||||
session.inUse = false
|
||||
|
||||
sessionController.deleteSession(session)
|
||||
|
||||
return Promise.resolve(jsonResult || webout)
|
||||
if (returnJson) return { result: debugResponse ?? webout, log }
|
||||
return debugResponse ?? webout
|
||||
}
|
||||
|
||||
buildDirectorytree() {
|
||||
@@ -162,3 +135,5 @@ ${webout}
|
||||
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 { configuration } from '../../../package.json'
|
||||
import { promisify } from 'util'
|
||||
import { execFile } from 'child_process'
|
||||
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../../utils'
|
||||
import {
|
||||
deleteFolder,
|
||||
createFile,
|
||||
fileExists,
|
||||
deleteFile,
|
||||
generateTimestamp
|
||||
} from '@sasjs/utils'
|
||||
import path from 'path'
|
||||
import { ExecutionController } from './Execution'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
export class SessionController {
|
||||
private sessions: Session[] = []
|
||||
private executionController: ExecutionController
|
||||
|
||||
constructor() {
|
||||
this.executionController = new ExecutionController()
|
||||
}
|
||||
|
||||
public async getSession() {
|
||||
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
|
||||
@@ -32,74 +30,87 @@ export class SessionController {
|
||||
|
||||
private async createSession() {
|
||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||
const sessionFolder = path.join(await 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 sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
1000
|
||||
).toString()
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
ready: false,
|
||||
creationTimeStamp: creationTimeStamp,
|
||||
deathTimeStamp: (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
1000
|
||||
).toString(),
|
||||
path: sessionFolder,
|
||||
inUse: false
|
||||
inUse: false,
|
||||
completed: false,
|
||||
creationTimeStamp,
|
||||
deathTimeStamp,
|
||||
path: sessionFolder
|
||||
}
|
||||
|
||||
// we do not want to leave sessions running forever
|
||||
// we clean them up after a predefined period, if unused
|
||||
this.scheduleSessionDestroy(session)
|
||||
|
||||
this.executionController
|
||||
.execute('', undefined, autoExec, session)
|
||||
.catch(() => {})
|
||||
// the autoexec file is executed on SAS startup
|
||||
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||
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)
|
||||
|
||||
// SAS has been triggered but we can't use it until
|
||||
// the autoexec deletes the code.sas file
|
||||
await this.waitForSession(session)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
public async waitForSession(session: Session) {
|
||||
if (await fileExists(path.join(session.path, 'code.sas'))) {
|
||||
while (await fileExists(path.join(session.path, 'code.sas'))) {}
|
||||
const codeFilePath = path.join(session.path, 'code.sas')
|
||||
|
||||
await deleteFile(path.join(session.path, 'log.log'))
|
||||
while (await fileExists(codeFilePath)) {}
|
||||
|
||||
session.ready = true
|
||||
|
||||
return Promise.resolve(session)
|
||||
} else {
|
||||
session.ready = true
|
||||
|
||||
return Promise.resolve(session)
|
||||
}
|
||||
session.ready = true
|
||||
return Promise.resolve(session)
|
||||
}
|
||||
|
||||
public async deleteSession(session: Session) {
|
||||
// remove the temporary files, to avoid buildup
|
||||
await deleteFolder(session.path)
|
||||
|
||||
// remove the session from the session array
|
||||
if (session.ready) {
|
||||
this.sessions = this.sessions.filter(
|
||||
(sess: Session) => sess.id !== session.id
|
||||
@@ -127,3 +138,19 @@ export const getSessionController = (): 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 {
|
||||
Request,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { ExecutionController } from './internal'
|
||||
import { PreProgramVars } from '../types'
|
||||
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
||||
import { request } from 'https'
|
||||
|
||||
interface ExecuteReturnJsonPayload {
|
||||
/**
|
||||
@@ -30,7 +31,7 @@ interface ExecuteReturnJsonResponse {
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/client')
|
||||
@Route('SASjsApi/stp')
|
||||
@Tags('STP')
|
||||
export class STPController {
|
||||
/**
|
||||
@@ -75,6 +76,7 @@ const executeReturnRaw = async (
|
||||
req: express.Request,
|
||||
_program: string
|
||||
): Promise<string> => {
|
||||
const query = req.query as { [key: string]: string | number | undefined }
|
||||
const sasCodePath =
|
||||
path
|
||||
.join(getTmpFilesFolderPath(), _program)
|
||||
@@ -84,20 +86,16 @@ const executeReturnRaw = async (
|
||||
const result = await new ExecutionController().execute(
|
||||
sasCodePath,
|
||||
getPreProgramVariables(req),
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
...req.query
|
||||
}
|
||||
query
|
||||
)
|
||||
|
||||
return result as string
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
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(
|
||||
sasCodePath,
|
||||
getPreProgramVariables(req),
|
||||
undefined,
|
||||
req.sasSession,
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true
|
||||
@@ -128,11 +124,11 @@ const executeReturnJson = async (
|
||||
result: jsonResult.result,
|
||||
log: jsonResult.log
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
status: 'failure',
|
||||
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,
|
||||
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 token = authHeader?.split(' ')[1]
|
||||
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 './desktopRestrict'
|
||||
export * from './verifyAdmin'
|
||||
export * from './verifyAdminIfNeeded'
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const verifyAdmin = (req: any, res: any, next: any) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') return next()
|
||||
|
||||
const { user } = req
|
||||
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
||||
next()
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from 'path'
|
||||
import { readFileSync } from 'fs'
|
||||
import * as https from 'https'
|
||||
import { configuration } from '../package.json'
|
||||
import app from './app'
|
||||
import appPromise from './app'
|
||||
|
||||
const keyPath = path.join('..', 'certificates', 'privkey.pem')
|
||||
const certPath = path.join('..', 'certificates', 'fullchain.pem')
|
||||
@@ -10,10 +10,12 @@ const certPath = path.join('..', 'certificates', 'fullchain.pem')
|
||||
const key = readFileSync(keyPath)
|
||||
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, () => {
|
||||
console.log(
|
||||
`⚡️[server]: Server is running at https://localhost:${configuration.sasJsPort}`
|
||||
)
|
||||
httpsServer.listen(configuration.sasJsPort, () => {
|
||||
console.log(
|
||||
`⚡️[server]: Server is running at https://localhost:${configuration.sasJsPort}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
authenticateRefreshToken
|
||||
} from '../../middlewares'
|
||||
|
||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||
import {
|
||||
authorizeValidation,
|
||||
getDesktopFields,
|
||||
tokenValidation
|
||||
} from '../../utils'
|
||||
import { InfoJWT } from '../../types'
|
||||
|
||||
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
|
||||
// we should exlcude connecting to the real database
|
||||
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) => {
|
||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import express from 'express'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import swaggerUi from 'swagger-ui-express'
|
||||
|
||||
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
desktopRestrict,
|
||||
verifyAdmin
|
||||
} from '../../middlewares'
|
||||
|
||||
import driveRouter from './drive'
|
||||
import stpRouter from './stp'
|
||||
import userRouter from './user'
|
||||
import groupRouter from './group'
|
||||
import clientRouter from './client'
|
||||
import authRouter, { connectDB } from './auth'
|
||||
|
||||
dotenv.config()
|
||||
connectDB()
|
||||
import authRouter from './auth'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/auth', authRouter)
|
||||
router.use('/client', authenticateAccessToken, verifyAdmin, clientRouter)
|
||||
router.use('/auth', desktopRestrict, authRouter)
|
||||
router.use(
|
||||
'/client',
|
||||
desktopRestrict,
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
clientRouter
|
||||
)
|
||||
router.use('/drive', authenticateAccessToken, driveRouter)
|
||||
router.use('/group', groupRouter)
|
||||
router.use('/group', desktopRestrict, groupRouter)
|
||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||
router.use('/user', userRouter)
|
||||
router.use('/user', desktopRestrict, userRouter)
|
||||
router.use(
|
||||
'/',
|
||||
swaggerUi.serve,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import app from '../../../app'
|
||||
import appPromise from '../../../app'
|
||||
import {
|
||||
UserController,
|
||||
ClientController,
|
||||
@@ -17,6 +18,11 @@ import {
|
||||
verifyTokenInDB
|
||||
} from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import app from '../../../app'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController, ClientController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const client = {
|
||||
clientId: 'someclientID',
|
||||
clientSecret: 'someclientSecret'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import app from '../../../app'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { getTreeExample } from '../../../controllers/internal'
|
||||
import { getTmpFilesFolderPath } from '../../../utils/file'
|
||||
@@ -10,6 +11,11 @@ import path from 'path'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
import { FolderMember, ServiceMember } from '../../../types'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import app from '../../../app'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import app from '../../../app'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFile } from '@sasjs/utils'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import { getWebBuildFolderPath } from '../../utils'
|
||||
@@ -5,7 +6,24 @@ import { getWebBuildFolderPath } from '../../utils'
|
||||
const webRouter = express.Router()
|
||||
|
||||
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
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import app from './app'
|
||||
import appPromise from './app'
|
||||
import { configuration } from '../package.json'
|
||||
|
||||
app.listen(configuration.sasJsPort, () => {
|
||||
console.log(
|
||||
`⚡️[server]: Server is running at http://localhost:${configuration.sasJsPort}`
|
||||
)
|
||||
appPromise.then((app) => {
|
||||
app.listen(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 {
|
||||
export interface Process {
|
||||
sasLoc?: string
|
||||
driveLoc?: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,6 @@ export interface Session {
|
||||
deathTimeStamp: string
|
||||
path: string
|
||||
inUse: boolean
|
||||
completed: boolean
|
||||
crashed?: boolean
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export const getWebBuildFolderPath = () =>
|
||||
getRealPath(path.join(__dirname, '..', '..', '..', 'web', 'build'))
|
||||
|
||||
export const getTmpFolderPath = () =>
|
||||
getRealPath(path.join(__dirname, '..', '..', 'tmp'))
|
||||
process.driveLoc ?? getRealPath(path.join(process.cwd(), 'tmp'))
|
||||
|
||||
export const getTmpFilesFolderPath = () =>
|
||||
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 './generateAuthCode'
|
||||
export * from './generateRefreshToken'
|
||||
export * from './getDesktopFields'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
export * from './sleep'
|
||||
|
||||
@@ -79,7 +79,7 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_program: Joi.string().required
|
||||
_program: Joi.string().required()
|
||||
})
|
||||
.pattern(/\w\d/, Joi.string())
|
||||
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
|
||||
.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'
|
||||
|
||||
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) => {
|
||||
return fetch('/SASjsApi/auth/authorize', {
|
||||
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify(credentials)
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const getTokens = async (payload: any) => {
|
||||
return fetch('/SASjsApi/auth/token', {
|
||||
return fetch(`${baseUrl}/SASjsApi/auth/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
@@ -43,15 +43,18 @@ export default function useTokens() {
|
||||
}
|
||||
}
|
||||
|
||||
// const baseUrl = 'http://localhost:5000'
|
||||
// const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
||||
const baseUrl =
|
||||
process.env.NODE_ENV === 'development' ? 'http://localhost:5000' : undefined
|
||||
|
||||
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
||||
|
||||
const setAxiosRequestHeader = (accessToken: string) => {
|
||||
axios.interceptors.request.use(function (config: any) {
|
||||
// if (!isAbsoluteURLRegex.test(config.url)) {
|
||||
// config.url = baseUrl + config.url
|
||||
// }
|
||||
config.headers.Authorization = `Bearer ${accessToken}`
|
||||
axios.interceptors.request.use(function (config) {
|
||||
if (baseUrl && !isAbsoluteURLRegex.test(config.url as string)) {
|
||||
config.url = baseUrl + config.url
|
||||
}
|
||||
config.headers!['Authorization'] = `Bearer ${accessToken}`
|
||||
config.withCredentials = true
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -20,7 +16,5 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user