1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-14 17:30:05 +00:00

Merge pull request #33 from sasjs/executables

Executables
This commit is contained in:
Muhammad Saad
2021-11-14 00:51:07 +05:00
committed by GitHub
37 changed files with 10840 additions and 14651 deletions

View File

@@ -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
View File

@@ -7,4 +7,5 @@ sas/
tmp/ tmp/
build/ build/
certificates/ certificates/
executables/
.env .env

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

File diff suppressed because it is too large Load Diff

View File

@@ -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: /

View File

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

View File

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

View File

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

View File

@@ -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;
`

View File

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

View File

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

View 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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {

View File

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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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