1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-12 11:54:35 +00:00

Compare commits

...

35 Commits

Author SHA1 Message Date
Allan Bowe
505f2089c7 chore(release): 0.0.52 2022-04-17 21:26:59 +00:00
Muhammad Saad
3344c400a8 Merge pull request #127 from sasjs/add-server-info-api-endpoint
feat: add api endpoint for getting server info
2022-04-17 12:48:12 -07:00
fa6248e3ef chore: swagger.yml updated 2022-04-17 23:53:20 +05:00
9fb5f1f8e7 feat: add api for getting server info 2022-04-17 23:48:08 +05:00
munja
92e0b8a088 chore(release): 0.0.51 2022-04-15 14:30:43 +01:00
Allan Bowe
b484306ed8 Merge pull request #126 from sasjs/issue-119
running code with CTRL+ENTER
2022-04-15 16:29:23 +03:00
5e08aacc51 chore: css fix 2022-04-15 14:53:36 +02:00
a9e4eb685d chore: style fix 2022-04-15 14:26:45 +02:00
31b09f27cc style: lint 2022-04-15 14:23:36 +02:00
9f3ec92f8e chore: run button style fix 2022-04-15 14:23:15 +02:00
6c9e449614 style: lint 2022-04-14 19:56:22 +02:00
68e84b0994 feat: run button running man, sub menu added 2022-04-14 19:38:44 +02:00
f0bb51a0d5 chore: placement of ctrl enter label 2022-04-13 22:12:40 +02:00
b93a0da3a3 feat: running code with CTRL+ENTER 2022-04-13 15:27:41 +02:00
Allan Bowe
e5facbf54c Update README.md 2022-04-13 12:24:42 +01:00
Allan Bowe
cb2bebbe76 Update README.md 2022-04-12 12:47:55 +01:00
Allan Bowe
9e1e0ce8cc chore(release): 0.0.50 2022-04-07 15:25:04 +00:00
Allan Bowe
29928753b7 Update CONTRIBUTING.md 2022-04-07 16:24:36 +01:00
Allan Bowe
edd69ecaae Merge pull request #122 from sasjs/issue121
Fixed couple of bugs + feature implemented
2022-04-07 18:23:34 +03:00
Saad Jutt
74ba65f9f3 feat(appstream): Upload an app from appStream page 2022-04-07 20:18:36 +05:00
Saad Jutt
f257602834 fix: web component UI fix for studio scrolling 2022-04-07 19:10:45 +05:00
Saad Jutt
61080d4694 fix: web component added tooltip for webout in studio 2022-04-07 18:59:31 +05:00
Saad Jutt
82633adbc4 chore: removed unused util 2022-04-07 18:48:31 +05:00
Saad Jutt
23db7e7b7d fix: session death time has to be a valid string number 2022-04-07 18:48:22 +05:00
Saad Jutt
cbaa687c9b chore(release): 0.0.49 2022-04-02 07:09:39 +05:00
Saad Jutt
527f70e90d fix(stp): read file in non-binary mode if debug one 2022-04-02 07:09:27 +05:00
Saad Jutt
122faad55f chore(release): 0.0.48 2022-04-02 07:06:18 +05:00
Saad Jutt
3ff6f5e865 fix(stp): return log+webout for debug on 2022-04-02 07:06:09 +05:00
Muhammad Saad
7d5128c0d6 Merge pull request #115 from sasjs/issue109
feat(deploy): new route added for deploy with build.json
2022-04-02 06:45:28 +05:00
Saad Jutt
e1ebbfd087 chore: increased file upload size to 100mb 2022-04-02 06:04:34 +05:00
Saad Jutt
e430bdb0d4 test(upload): spec updated for file upload exceeding limit 2022-04-02 05:51:24 +05:00
Saad Jutt
9d9769eef3 chore: increased file upload size to 100mb 2022-04-02 05:36:53 +05:00
Saad Jutt
9d167abe2a fix: remove uploaded build.json from temp folder in all cases 2022-04-02 05:29:34 +05:00
Saad Jutt
18d0604bdd feat(deploy): new route added for deploy with build.json 2022-04-02 05:23:25 +05:00
Saad Jutt
7b7bc6b778 chore: fix vulnerabilities 2022-03-31 01:54:40 +05:00
37 changed files with 548 additions and 203 deletions

View File

@@ -113,3 +113,7 @@ cd ./api && npm i && npm run exe
``` ```
This will install/build web app and install/create executables of sasjs server at root `./executables` This will install/build web app and install/create executables of sasjs server at root `./executables`
## Releases
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)

View File

@@ -2,6 +2,55 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
### Features
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
### Features
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
### Features
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
### Bug Fixes
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
### Bug Fixes
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
### Features
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
### Bug Fixes
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29) ### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)

View File

@@ -52,6 +52,7 @@ Example contents of a `.env` file:
MODE= MODE=
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop` # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS= CORS=
# options: <http://localhost:3000 https://abc.com ...> space separated urls # options: <http://localhost:3000 https://abc.com ...> space separated urls
@@ -84,6 +85,15 @@ ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret> REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret> AUTH_CODE_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
# SAS Options
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
# Any options set here are automatically applied in the SAS session
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
SAS_OPTIONS= -NOXCMD
SASV9_OPTIONS= -NOXCMD
``` ```
## Persisting the Session ## Persisting the Session

14
api/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.2", "version": "0.0.2",
"dependencies": { "dependencies": {
"@sasjs/core": "4.9.0", "@sasjs/core": "4.9.0",
"@sasjs/utils": "2.36.2", "@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -1384,9 +1384,9 @@
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ==" "integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.36.2", "version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz", "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==", "integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
@@ -11132,9 +11132,9 @@
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ==" "integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.36.2", "version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz", "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==", "integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"requires": { "requires": {
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13", "@types/prompts": "2.0.13",

View File

@@ -47,7 +47,7 @@
"author": "4GL Ltd", "author": "4GL Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "4.9.0", "@sasjs/core": "4.9.0",
"@sasjs/utils": "2.36.2", "@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",

BIN
api/public/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

View File

@@ -418,6 +418,25 @@ components:
example: /Public/somefolder/some.file example: /Public/somefolder/some.file
type: object type: object
additionalProperties: false additionalProperties: false
InfoResponse:
properties:
mode:
type: string
cors:
type: string
whiteList:
items:
type: string
type: array
protocol:
type: string
required:
- mode
- cors
- whiteList
- protocol
type: object
additionalProperties: false
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
type: http type: http
@@ -606,6 +625,56 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/DeployPayload' $ref: '#/components/schemas/DeployPayload'
/SASjsApi/drive/deploy/upload:
post:
operationId: DeployUpload
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
'400':
description: 'Invalid Format'
content:
application/json:
schema:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Provided not supported data format.'}
'500':
description: 'Execution Error'
content:
application/json:
schema:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Deployment failed!'}
summary: 'Creates/updates files within SASjs Drive using uploaded JSON file.'
tags:
- Drive
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
required:
- file
/SASjsApi/drive/file: /SASjsApi/drive/file:
get: get:
operationId: GetFile operationId: GetFile
@@ -1190,10 +1259,31 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteReturnJsonPayload' $ref: '#/components/schemas/ExecuteReturnJsonPayload'
/SASjsApi/info:
get:
operationId: Info
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/InfoResponse'
examples:
'Example 1':
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http}
summary: 'Get server info (mode, cors, whiteList, protocol).'
tags:
- Info
security: []
parameters: []
servers: servers:
- -
url: / url: /
tags: tags:
-
name: Info
description: 'Get Server Info'
- -
name: Session name: Session
description: 'Get Session information' description: 'Get Session information'

View File

@@ -1,5 +1,6 @@
import path from 'path' import path from 'path'
import { import {
CompileTree,
createFile, createFile,
loadDependenciesFile, loadDependenciesFile,
readFile, readFile,
@@ -18,7 +19,8 @@ const compiledSystemInit = async (systemInit: string) =>
macroFolders: [], macroFolders: [],
buildSourceFolder: '', buildSourceFolder: '',
binaryFolders: [], binaryFolders: [],
macroCorePath macroCorePath,
compileTree: new CompileTree('') // dummy compileTree
})) }))
const createSysInitFile = async () => { const createSysInitFile = async () => {

View File

@@ -14,7 +14,8 @@ import {
Patch, Patch,
UploadedFile, UploadedFile,
FormField, FormField,
Delete Delete,
Hidden
} from 'tsoa' } from 'tsoa'
import { import {
fileExists, fileExists,
@@ -22,14 +23,15 @@ import {
createFolder, createFolder,
deleteFile as deleteFileOnSystem, deleteFile as deleteFileOnSystem,
folderExists, folderExists,
listFilesAndSubFoldersInFolder,
listFilesInFolder, listFilesInFolder,
listSubFoldersInFolder, listSubFoldersInFolder,
isFolder isFolder,
FileTree,
isFileTree
} from '@sasjs/utils' } from '@sasjs/utils'
import { createFileTree, ExecutionController, getTreeExample } from './internal' import { createFileTree, ExecutionController, getTreeExample } from './internal'
import { FileTree, isFileTree, TreeNode } from '../types' import { TreeNode } from '../types'
import { getTmpFilesFolderPath } from '../utils' import { getTmpFilesFolderPath } from '../utils'
interface DeployPayload { interface DeployPayload {
@@ -93,6 +95,21 @@ export class DriveController {
return deploy(body) return deploy(body)
} }
/**
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
*
*/
@Example<DeployResponse>(successDeployResponse)
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
@Post('/deploy/upload')
public async deployUpload(
@UploadedFile() file: Express.Multer.File, // passing here for API docs
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
): Promise<DeployResponse> {
return deploy(body!)
}
/** /**
* *
* @summary Get file from SASjs Drive * @summary Get file from SASjs Drive

View File

@@ -6,3 +6,4 @@ export * from './group'
export * from './session' export * from './session'
export * from './stp' export * from './stp'
export * from './user' export * from './user'
export * from './info'

View File

@@ -0,0 +1,37 @@
import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
export interface InfoResponse {
mode: string
cors: string
whiteList: string[]
protocol: string
}
@Route('SASjsApi/info')
@Tags('Info')
export class InfoController {
/**
* @summary Get server info (mode, cors, whiteList, protocol).
*
*/
@Example<InfoResponse>({
mode: 'desktop',
cors: 'enable',
whiteList: ['http://example.com', 'http://example2.com'],
protocol: 'http'
})
@Get('/')
public info(): InfoResponse {
const response = {
mode: process.env.MODE ?? 'desktop',
cors:
process.env.CORS ?? process.env.MODE === 'server'
? 'disable'
: 'enable',
whiteList: process.env.WHITELIST?.split(' ') ?? [],
protocol: process.env.PROTOCOL ?? 'http'
}
return response
}
}

View File

@@ -157,7 +157,9 @@ ${program}`
: '' : ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent) const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean = const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') && !returnJson httpHeaders.hasOwnProperty('content-type') &&
!returnJson && // not a POST Request
!isDebugOn(vars) // Debug is not enabled
const webout = (await fileExists(weboutPath)) const webout = (await fileExists(weboutPath))
? fileResponse ? fileResponse
@@ -178,9 +180,8 @@ ${program}`
return { return {
httpHeaders, httpHeaders,
result: fileResponse result:
? webout isDebugOn(vars) || session.crashed
: isDebugOn(vars) || session.crashed
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>` ? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
: webout : webout
} }

View File

@@ -12,8 +12,7 @@ import {
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile, readFile
moveFile
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -41,6 +40,7 @@ export class SessionController {
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId) const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = ( const deathTimeStamp = (
parseInt(creationTimeStamp) + parseInt(creationTimeStamp) +
15 * 60 * 1000 - 15 * 60 * 1000 -
@@ -140,7 +140,9 @@ ${autoExecContent}`
private scheduleSessionDestroy(session: Session) { private scheduleSessionDestroy(session: Session) {
setTimeout(async () => { setTimeout(async () => {
if (session.inUse) { if (session.inUse) {
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10 // adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session) this.scheduleSessionDestroy(session)
} else { } else {

View File

@@ -1,13 +1,15 @@
import path from 'path' import path from 'path'
import { getTmpFilesFolderPath } from '../../utils/file'
import { import {
MemberType, createFolder,
createFile,
asyncForEach,
FolderMember, FolderMember,
ServiceMember, ServiceMember,
FileTree, FileMember,
FileMember MemberType,
} from '../../types' FileTree
import { getTmpFilesFolderPath } from '../../utils/file' } from '@sasjs/utils'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
// REFACTOR: export FileTreeCpntroller // REFACTOR: export FileTreeCpntroller
export const createFileTree = async ( export const createFileTree = async (

View File

@@ -4,7 +4,7 @@ import multer, { FileFilterCallback, Options } from 'multer'
import { blockFileRegex, getTmpUploadsPath } from '../utils' import { blockFileRegex, getTmpUploadsPath } from '../utils'
const fieldNameSize = 300 const fieldNameSize = 300
const fileSize = 10485760 // 10 MB const fileSize = 104857600 // 100 MB
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: getTmpUploadsPath(), destination: getTmpUploadsPath(),

View File

@@ -1,5 +1,5 @@
import express from 'express' import express from 'express'
import { deleteFile } from '@sasjs/utils' import { deleteFile, readFile } from '@sasjs/utils'
import { publishAppStream } from '../appStream' import { publishAppStream } from '../appStream'
@@ -43,6 +43,54 @@ driveRouter.post('/deploy', async (req, res) => {
} }
}) })
driveRouter.post(
'/deploy/upload',
(...arg) => multerSingle('file', arg),
async (req, res) => {
if (!req.file) return res.status(400).send('"file" is not present.')
const fileContent = await readFile(req.file.path)
let jsonContent
try {
jsonContent = JSON.parse(fileContent)
} catch (err) {
deleteFile(req.file.path)
return res.status(400).send('File containing invalid JSON content.')
}
const { error, value: body } = deployValidation(jsonContent)
if (error) {
deleteFile(req.file.path)
return res.status(400).send(error.details[0].message)
}
try {
const response = await controller.deployUpload(req.file, body)
if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
} finally {
deleteFile(req.file.path)
}
}
)
driveRouter.get('/file', async (req, res) => { driveRouter.get('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query) const { error: errQ, value: query } = fileParamValidation(req.query)

View File

@@ -9,6 +9,7 @@ import {
verifyAdmin verifyAdmin
} from '../../middlewares' } from '../../middlewares'
import infoRouter from './info'
import driveRouter from './drive' import driveRouter from './drive'
import stpRouter from './stp' import stpRouter from './stp'
import codeRouter from './code' import codeRouter from './code'
@@ -20,6 +21,7 @@ import sessionRouter from './session'
const router = express.Router() const router = express.Router()
router.use('/info', infoRouter)
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter) router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter) router.use('/auth', desktopRestrict, authRouter)
router.use( router.use(

View File

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

View File

@@ -12,7 +12,9 @@ import {
generateTimestamp, generateTimestamp,
copy, copy,
createFolder, createFolder,
createFile createFile,
ServiceMember,
FolderMember
} from '@sasjs/utils' } from '@sasjs/utils'
import * as fileUtilModules from '../../../utils/file' import * as fileUtilModules from '../../../utils/file'
@@ -28,7 +30,6 @@ jest
import appPromise 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 { FolderMember, ServiceMember } from '../../../types'
import { generateAccessToken, saveTokensInDB } from '../../../utils/' import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getTmpFilesFolderPath } = fileUtilModules const { getTmpFilesFolderPath } = fileUtilModules
@@ -424,7 +425,7 @@ describe('drive', () => {
it('should respond with Bad Request if attached file exceeds file limit', async () => { it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas' const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024)) const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
const res = await request(app) const res = await request(app)
.post('/SASjsApi/drive/file') .post('/SASjsApi/drive/file')
@@ -434,7 +435,7 @@ describe('drive', () => {
.expect(400) .expect(400)
expect(res.text).toEqual( expect(res.text).toEqual(
'File size is over limit. File limit is: 10 MB' 'File size is over limit. File limit is: 100 MB'
) )
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -582,7 +583,7 @@ describe('drive', () => {
it('should respond with Bad Request if attached file exceeds file limit', async () => { it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas' const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024)) const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
const res = await request(app) const res = await request(app)
.patch('/SASjsApi/drive/file') .patch('/SASjsApi/drive/file')
@@ -592,7 +593,7 @@ describe('drive', () => {
.expect(400) .expect(400)
expect(res.text).toEqual( expect(res.text).toEqual(
'File size is over limit. File limit is: 10 MB' 'File size is over limit. File limit is: 100 MB'
) )
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })

View File

@@ -0,0 +1,14 @@
import { Express } from 'express'
import request from 'supertest'
import appPromise from '../../../app'
let app: Express
describe('Info', () => {
it('should should return configured information of the server instance', async () => {
await appPromise.then((_app) => {
app = _app
})
request(app).get('/SASjsApi/info').expect(200)
})
})

View File

@@ -1,27 +1,6 @@
import { AppStreamConfig } from '../../types' import { AppStreamConfig } from '../../types'
import { script } from './script'
const style = `<style> import { style } from './style'
* {
font-family: 'Roboto', sans-serif;
}
.app-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
.app-container .app {
width: 150px;
margin: 10px;
overflow: hidden;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
border-radius: 10px;
}
</style>`
const defaultAppLogo = '/sasjs-logo.svg' const defaultAppLogo = '/sasjs-logo.svg'
@@ -52,6 +31,14 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo) singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
) )
.join('')} .join('')}
<a class="app" title="Upload build.json">
<input id="fileId" type="file" hidden />
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
<img src="/plus.png" />
</button>
<span id="uploadMessage">Upload New App</span>
</a>
</div> </div>
${script}
</body> </body>
</html>` </html>`

View File

@@ -52,7 +52,7 @@ export const publishAppStream = async (
addEntryToFile addEntryToFile
) )
const sasJsPort = process.env.PORT ?? 5000 const sasJsPort = process.env.PORT || 5000
console.log( console.log(
'Serving Stream App: ', 'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}` `http://localhost:${sasJsPort}/AppStream/${streamServiceName}`

View File

@@ -0,0 +1,58 @@
export const script = `<script>
const inputElement = document.getElementById('fileId')
document
.getElementById('uploadButton')
.addEventListener('click', function () {
inputElement.click()
})
inputElement.addEventListener(
'change',
function () {
const fileList = this.files /* now you can work with the file list */
updateFileUploadMessage('Requesting ...')
const file = fileList[0]
const formData = new FormData()
formData.append('file', file)
fetch('/SASjsApi/drive/deploy/upload', {
method: 'POST',
body: formData
})
.then(async (res) => {
const { status, ok } = res
if (status === 200 && ok) {
const data = await res.json()
return (
data.message +
'\\nstreamServiceName: ' +
data.streamServiceName +
'\\nrefreshing page once alert box closes.'
)
}
throw await res.text()
})
.then((message) => {
alert(message)
location.reload()
})
.catch((error) => {
alert(error)
resetFileUpload()
updateFileUploadMessage('Upload New App')
})
},
false
)
function updateFileUploadMessage(message) {
document.getElementById('uploadMessage').innerHTML = message
}
function resetFileUpload() {
inputElement.value = null
}
</script>`

View File

@@ -0,0 +1,22 @@
export const style = `<style>
* {
font-family: 'Roboto', sans-serif;
}
.app-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
.app-container .app {
width: 150px;
margin: 10px;
overflow: hidden;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
border-radius: 10px;
}
</style>`

View File

@@ -4,8 +4,8 @@ import appPromise from './app'
import { getCertificates } from './utils' import { getCertificates } from './utils'
appPromise.then(async (app) => { appPromise.then(async (app) => {
const protocol = process.env.PROTOCOL ?? 'http' const protocol = process.env.PROTOCOL || 'http'
const sasJsPort = process.env.PORT ?? 5000 const sasJsPort = process.env.PORT || 5000
console.log('PROTOCOL: ', protocol) console.log('PROTOCOL: ', protocol)

View File

@@ -1,62 +0,0 @@
export enum MemberType {
service = 'service',
file = 'file',
folder = 'folder'
}
export interface ServiceMember {
name: string
type: MemberType.service
code: string
}
export interface FileMember {
name: string
type: MemberType.file
code: string
}
export interface FolderMember {
name: string
type: MemberType.folder
members: (FolderMember | ServiceMember | FileMember)[]
}
export interface FileTree {
members: (FolderMember | ServiceMember | FileMember)[]
}
export const isFileTree = (arg: any): arg is FileTree =>
arg &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: ServiceMember | FileMember | FolderMember) =>
!isServiceMember(member, '-') &&
!isFileMember(member, '-') &&
!isFolderMember(member, '-')
).length === 0
const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
typeof arg.code === 'string'
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.file &&
typeof arg.code === 'string'
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.folder &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isServiceMember(member, pre + '-') &&
!isFileMember(member, pre + '-') &&
!isFolderMember(member, pre + '-')
).length === 0

View File

@@ -1,7 +1,6 @@
// TODO: uppercase types // TODO: uppercase types
export * from './AppStreamConfig' export * from './AppStreamConfig'
export * from './Execution' export * from './Execution'
export * from './FileTree'
export * from './InfoJWT' export * from './InfoJWT'
export * from './PreProgramVars' export * from './PreProgramVars'
export * from './Request' export * from './Request'

View File

@@ -14,7 +14,6 @@ export * from './removeTokensInDB'
export * from './saveTokensInDB' export * from './saveTokensInDB'
export * from './setProcessVariables' export * from './setProcessVariables'
export * from './setupFolders' export * from './setupFolders'
export * from './sleep'
export * from './upload' export * from './upload'
export * from './validation' export * from './validation'
export * from './verifyTokenInDB' export * from './verifyTokenInDB'

View File

@@ -1,3 +0,0 @@
export const sleep = async (delay: number) => {
await new Promise((resolve) => setTimeout(resolve, delay))
}

View File

@@ -11,6 +11,10 @@
} }
}, },
"tags": [ "tags": [
{
"name": "Info",
"description": "Get Server Info"
},
{ {
"name": "Session", "name": "Session",
"description": "Get Session information" "description": "Get Session information"

16
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "server", "name": "server",
"version": "0.0.47", "version": "0.0.52",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "server", "name": "server",
"version": "0.0.47", "version": "0.0.52",
"devDependencies": { "devDependencies": {
"prettier": "^2.3.1", "prettier": "^2.3.1",
"standard-version": "^9.3.2" "standard-version": "^9.3.2"
@@ -1350,9 +1350,9 @@
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true "dev": true
}, },
"node_modules/minimist-options": { "node_modules/minimist-options": {
@@ -3158,9 +3158,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true "dev": true
}, },
"minimist-options": { "minimist-options": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.0.47", "version": "0.0.52",
"description": "NodeJS wrapper for calling the SAS binary executable", "description": "NodeJS wrapper for calling the SAS binary executable",
"repository": "https://github.com/sasjs/server", "repository": "https://github.com/sasjs/server",
"scripts": { "scripts": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
web/public/running-sas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -28,10 +28,10 @@ const Header = (props: any) => {
> >
<Toolbar variant="dense"> <Toolbar variant="dense">
<img <img
src="logo-white.png" src="logo.png"
alt="logo" alt="logo"
style={{ style={{
width: '50px', width: '35px',
cursor: 'pointer', cursor: 'pointer',
marginRight: '25px' marginRight: '25px'
}} }}

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
import axios from 'axios' import axios from 'axios'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import { Button, Paper, Stack, Tab } from '@mui/material' import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material'
import { makeStyles } from '@mui/styles' import { makeStyles } from '@mui/styles'
import Editor, { OnMount } from '@monaco-editor/react' import Editor, { OnMount } from '@monaco-editor/react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
@@ -15,6 +15,17 @@ const useStyles = makeStyles(() => ({
'&.Mui-selected': { '&.Mui-selected': {
color: 'black' color: 'black'
} }
},
subMenu: {
marginTop: '25px',
display: 'flex',
justifyContent: 'center'
},
runButton: {
display: 'flex',
alignItems: 'center',
padding: '5px 5px',
minWidth: 'unset'
} }
})) }))
@@ -22,8 +33,10 @@ const Studio = () => {
const location = useLocation() const location = useLocation()
const [fileContent, setFileContent] = useState('') const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('') const [log, setLog] = useState('')
const [ctrlPressed, setCtrlPressed] = useState(false)
const [webout, setWebout] = useState('') const [webout, setWebout] = useState('')
const [tab, setTab] = React.useState('1') const [tab, setTab] = React.useState('1')
const handleTabChange = (_e: any, newValue: string) => { const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue) setTab(newValue)
} }
@@ -61,6 +74,21 @@ const Studio = () => {
.catch((err) => console.log(err)) .catch((err) => console.log(err))
} }
const handleKeyDown = (event: any) => {
if (event.ctrlKey) {
if (event.key === 'v') {
setCtrlPressed(false)
}
if (event.key === 'Enter') runCode(getSelection() || fileContent)
if (!ctrlPressed) setCtrlPressed(true)
}
}
const handleKeyUp = (event: any) => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
}
useEffect(() => { useEffect(() => {
const content = localStorage.getItem('fileContent') ?? '' const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content) setFileContent(content)
@@ -86,11 +114,11 @@ const Studio = () => {
const classes = useStyles() const classes = useStyles()
return ( return (
<> <Box
<br /> onKeyUp={handleKeyUp}
<br /> onKeyDown={handleKeyDown}
<br /> sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
<Box sx={{ width: '100%', typography: 'body1' }}> >
<TabContext value={tab}> <TabContext value={tab}>
<Box <Box
sx={{ sx={{
@@ -102,15 +130,29 @@ const Studio = () => {
<TabList onChange={handleTabChange} centered> <TabList onChange={handleTabChange} centered>
<Tab className={classes.root} label="Code" value="1" /> <Tab className={classes.root} label="Code" value="1" />
<Tab className={classes.root} label="Log" value="2" /> <Tab className={classes.root} label="Log" value="2" />
<Tooltip title="Displays content from the _webout fileref">
<Tab className={classes.root} label="Webout" value="3" /> <Tab className={classes.root} label="Webout" value="3" />
</Tooltip>
</TabList> </TabList>
</Box> </Box>
<TabPanel value="1">
<TabPanel style={{ paddingBottom: 0 }} value="1">
<div className={classes.subMenu}>
<Tooltip title="CTRL+ENTER will also run SAS code">
<Button onClick={handleRunBtnClick} className={classes.runButton}>
<img
draggable="false"
style={{ width: '25px' }}
src="/running-sas.png"
></img>
<span style={{ fontSize: '12px' }}>RUN</span>
</Button>
</Tooltip>
</div>
{/* <Toolbar /> */} {/* <Toolbar /> */}
<Paper <Paper
sx={{ sx={{
height: '70vh', height: 'calc(100vh - 170px)',
marginTop: '50px',
padding: '10px', padding: '10px',
overflow: 'auto', overflow: 'auto',
position: 'relative' position: 'relative'
@@ -118,23 +160,27 @@ const Studio = () => {
elevation={3} elevation={3}
> >
<Editor <Editor
height="95%" height="98%"
value={fileContent} value={fileContent}
onMount={handleEditorDidMount} onMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => { onChange={(val) => {
if (val) setFileContent(val) if (val) setFileContent(val)
}} }}
/> />
</Paper> <p
<Stack style={{
spacing={3} position: 'absolute',
direction="row" left: 0,
sx={{ justifyContent: 'center', marginTop: '20px' }} right: 0,
bottom: -10,
textAlign: 'center',
fontSize: '13px'
}}
> >
<Button variant="contained" onClick={handleRunBtnClick}> Press CTRL + ENTER to run SAS code
Run SAS Code </p>
</Button> </Paper>
</Stack>
</TabPanel> </TabPanel>
<TabPanel value="2"> <TabPanel value="2">
<div style={{ marginTop: '50px' }}> <div style={{ marginTop: '50px' }}>
@@ -149,7 +195,6 @@ const Studio = () => {
</TabPanel> </TabPanel>
</TabContext> </TabContext>
</Box> </Box>
</>
) )
} }