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

chore: merge main into issue-198

This commit is contained in:
2022-07-22 22:31:32 +05:00
19 changed files with 2281 additions and 3099 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [sasjs]

View File

@@ -1,3 +1,51 @@
## [0.11.5](https://github.com/sasjs/server/compare/v0.11.4...v0.11.5) (2022-07-19)
### Bug Fixes
* Revert "fix(security): missing cookie flags are added" ([ce5218a](https://github.com/sasjs/server/commit/ce5218a2278cc750f2b1032024685dc6cd72f796))
## [0.11.4](https://github.com/sasjs/server/compare/v0.11.3...v0.11.4) (2022-07-19)
### Bug Fixes
* **security:** missing cookie flags are added ([526402f](https://github.com/sasjs/server/commit/526402fd73407ee4fa2d31092111a7e6a1741487))
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)
### Bug Fixes
* filePath fix in code.js file for windows ([2995121](https://github.com/sasjs/server/commit/299512135d77c2ac9e34853cf35aee6f2e1d4da4))
## [0.11.2](https://github.com/sasjs/server/compare/v0.11.1...v0.11.2) (2022-07-18)
### Bug Fixes
* apply icon option only for sas.exe ([d2ddd8a](https://github.com/sasjs/server/commit/d2ddd8aacadfdd143026881f2c6ae8c6b277610a))
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)
### Bug Fixes
* bank operator ([aa02741](https://github.com/sasjs/server/commit/aa027414ed3ce51f1014ef36c4191e064b2e963d))
* ensuring nosplash option only applies for sas.exe ([65e6de9](https://github.com/sasjs/server/commit/65e6de966383fe49a919b1f901d77c7f1e402c9b)), closes [#229](https://github.com/sasjs/server/issues/229)
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)
### Bug Fixes
* **logs:** logs location is configurable ([e024a92](https://github.com/sasjs/server/commit/e024a92f165990e08db8aa26ee326dbcb30e2e46))
### Features
* **logs:** logs to file with rotating + code split into files ([92fda18](https://github.com/sasjs/server/commit/92fda183f3f0f3956b7c791669eb8dd52c389d1b))
# [0.10.0](https://github.com/sasjs/server/compare/v0.9.0...v0.10.0) (2022-07-06) # [0.10.0](https://github.com/sasjs/server/compare/v0.9.0...v0.10.0) (2022-07-06)

View File

@@ -136,6 +136,9 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
# Docs: https://www.npmjs.com/package/morgan#predefined-formats # Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN= LOG_FORMAT_MORGAN=
# This location is for server logs with classical UNIX logrotate behavior
LOG_LOCATION=./sasjs_root/logs
# A comma separated string that defines the available runTimes. # A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string. # Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas and js # Possible options at the moment are sas and js

View File

@@ -21,3 +21,4 @@ NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
SASJS_ROOT=./sasjs_root SASJS_ROOT=./sasjs_root
LOG_FORMAT_MORGAN=common LOG_FORMAT_MORGAN=common
LOG_LOCATION=./sasjs_root/logs

1252
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore", "initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
"prestart": "npm run initial", "prestart": "npm run initial",
"prebuild": "npm run initial", "prebuild": "npm run initial",
"start": "nodemon ./src/server.ts", "start": "NODE_ENV=development nodemon ./src/server.ts",
"start:prod": "node ./build/src/server.js", "start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc", "build": "rimraf build && tsc",
"postbuild": "npm run copy:files", "postbuild": "npm run copy:files",
@@ -63,6 +63,7 @@
"mongoose-sequence": "^5.3.1", "mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.3", "multer": "^1.4.3",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0", "swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11", "unzipper": "^0.10.11",
"url": "^0.10.3" "url": "^0.10.3"

View File

@@ -85,10 +85,8 @@ components:
type: string type: string
_webout: _webout:
anyOf: anyOf:
- - type: string
type: string - $ref: '#/components/schemas/IRecordOfAny'
-
$ref: '#/components/schemas/IRecordOfAny'
log: log:
items: items:
$ref: '#/components/schemas/LogLine' $ref: '#/components/schemas/LogLine'
@@ -137,12 +135,9 @@ components:
members: members:
items: items:
anyOf: anyOf:
- - $ref: '#/components/schemas/FolderMember'
$ref: '#/components/schemas/FolderMember' - $ref: '#/components/schemas/ServiceMember'
- - $ref: '#/components/schemas/FileMember'
$ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
type: array type: array
required: required:
- name - name
@@ -191,12 +186,9 @@ components:
members: members:
items: items:
anyOf: anyOf:
- - $ref: '#/components/schemas/FolderMember'
$ref: '#/components/schemas/FolderMember' - $ref: '#/components/schemas/ServiceMember'
- - $ref: '#/components/schemas/FileMember'
$ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
type: array type: array
required: required:
- members - members
@@ -382,7 +374,7 @@ components:
autoExec: autoExec:
type: string type: string
description: 'User-specific auto-exec code' description: 'User-specific auto-exec code'
example: "" example: ''
required: required:
- displayName - displayName
- username - username
@@ -619,7 +611,11 @@ paths:
$ref: '#/components/schemas/TokenResponse' $ref: '#/components/schemas/TokenResponse'
examples: examples:
'Example 1': 'Example 1':
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString} value:
{
accessToken: someRandomCryptoString,
refreshToken: someRandomCryptoString
}
summary: 'Accepts client/auth code and returns access/refresh tokens' summary: 'Accepts client/auth code and returns access/refresh tokens'
tags: tags:
- Auth - Auth
@@ -643,13 +639,16 @@ paths:
$ref: '#/components/schemas/TokenResponse' $ref: '#/components/schemas/TokenResponse'
examples: examples:
'Example 1': 'Example 1':
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString} value:
{
accessToken: someRandomCryptoString,
refreshToken: someRandomCryptoString
}
summary: 'Returns new access/refresh tokens' summary: 'Returns new access/refresh tokens'
tags: tags:
- Auth - Auth
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
/SASjsApi/auth/logout: /SASjsApi/auth/logout:
post: post:
@@ -661,8 +660,7 @@ paths:
tags: tags:
- Auth - Auth
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
/SASjsApi/client: /SASjsApi/client:
post: post:
@@ -676,13 +674,16 @@ paths:
$ref: '#/components/schemas/ClientPayload' $ref: '#/components/schemas/ClientPayload'
examples: examples:
'Example 1': 'Example 1':
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString} value:
{
clientId: someFormattedClientID1234,
clientSecret: someRandomCryptoString
}
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.' summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
tags: tags:
- Client - Client
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -705,8 +706,7 @@ paths:
tags: tags:
- CODE - CODE
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -726,7 +726,11 @@ paths:
$ref: '#/components/schemas/DeployResponse' $ref: '#/components/schemas/DeployResponse'
examples: examples:
'Example 1': 'Example 1':
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'} value:
{
status: success,
message: 'Files deployed successfully to @sasjs/server.'
}
'400': '400':
description: 'Invalid Format' description: 'Invalid Format'
content: content:
@@ -735,7 +739,11 @@ paths:
$ref: '#/components/schemas/DeployResponse' $ref: '#/components/schemas/DeployResponse'
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'Provided not supported data format.'} value:
{
status: failure,
message: 'Provided not supported data format.'
}
'500': '500':
description: 'Execution Error' description: 'Execution Error'
content: content:
@@ -749,8 +757,7 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -770,7 +777,11 @@ paths:
$ref: '#/components/schemas/DeployResponse' $ref: '#/components/schemas/DeployResponse'
examples: examples:
'Example 1': 'Example 1':
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'} value:
{
status: success,
message: 'Files deployed successfully to @sasjs/server.'
}
'400': '400':
description: 'Invalid Format' description: 'Invalid Format'
content: content:
@@ -779,7 +790,11 @@ paths:
$ref: '#/components/schemas/DeployResponse' $ref: '#/components/schemas/DeployResponse'
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'Provided not supported data format.'} value:
{
status: failure,
message: 'Provided not supported data format.'
}
'500': '500':
description: 'Execution Error' description: 'Execution Error'
content: content:
@@ -794,8 +809,7 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -819,11 +833,9 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - in: query
in: query
name: _filePath name: _filePath
required: true required: true
schema: schema:
@@ -846,11 +858,9 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - in: query
in: query
name: _filePath name: _filePath
required: true required: true
schema: schema:
@@ -882,11 +892,9 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: 'Location of file'
description: 'Location of file'
in: query in: query
name: _filePath name: _filePath
required: false required: false
@@ -920,7 +928,7 @@ paths:
'Example 1': 'Example 1':
value: { status: success } value: { status: success }
'403': '403':
description: "" description: ''
content: content:
application/json: application/json:
schema: schema:
@@ -933,11 +941,9 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: 'Location of SAS program'
description: 'Location of SAS program'
in: query in: query
name: _filePath name: _filePath
required: false required: false
@@ -978,11 +984,9 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - in: query
in: query
name: _folderPath name: _folderPath
required: false required: false
schema: schema:
@@ -1005,11 +1009,9 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - in: query
in: query
name: _folderPath name: _folderPath
required: true required: true
schema: schema:
@@ -1035,13 +1037,13 @@ paths:
$ref: '#/components/schemas/FileFolderResponse' $ref: '#/components/schemas/FileFolderResponse'
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'Add folder request failed.'} value:
{ status: failure, message: 'Add folder request failed.' }
summary: 'Create an empty folder in SASjs Drive' summary: 'Create an empty folder in SASjs Drive'
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -1075,8 +1077,7 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -1098,8 +1099,7 @@ paths:
tags: tags:
- Drive - Drive
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
/SASjsApi/user: /SASjsApi/user:
get: get:
@@ -1115,13 +1115,26 @@ paths:
type: array type: array
examples: examples:
'Example 1': 'Example 1':
value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}] value:
[
{
id: 123,
username: johnusername,
displayName: John,
isAdmin: false
},
{
id: 456,
username: starkusername,
displayName: Stark,
isAdmin: true
}
]
summary: 'Get list of all users (username, displayname). All users can request this.' summary: 'Get list of all users (username, displayname). All users can request this.'
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
post: post:
operationId: CreateUser operationId: CreateUser
@@ -1134,13 +1147,19 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse' $ref: '#/components/schemas/UserDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} value:
{
id: 1234,
displayName: 'John Snow',
username: johnSnow01,
isAdmin: false,
isActive: true
}
summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.' summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.'
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -1163,11 +1182,9 @@ paths:
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The User's username"
description: 'The User''s username'
in: path in: path
name: username name: username
required: true required: true
@@ -1185,16 +1202,21 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse' $ref: '#/components/schemas/UserDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} value:
{
id: 1234,
displayName: 'John Snow',
username: johnSnow01,
isAdmin: false,
isActive: true
}
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The User's username"
description: 'The User''s username'
in: path in: path
name: username name: username
required: true required: true
@@ -1216,11 +1238,9 @@ paths:
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The User's username"
description: 'The User''s username'
in: path in: path
name: username name: username
required: true required: true
@@ -1251,11 +1271,9 @@ paths:
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The user's identifier"
description: 'The user''s identifier'
in: path in: path
name: userId name: userId
required: true required: true
@@ -1274,16 +1292,21 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse' $ref: '#/components/schemas/UserDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} value:
{
id: 1234,
displayName: 'John Snow',
username: johnSnow01,
isAdmin: false,
isActive: true
}
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The user's identifier"
description: 'The user''s identifier'
in: path in: path
name: userId name: userId
required: true required: true
@@ -1306,11 +1329,9 @@ paths:
tags: tags:
- User - User
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The user's identifier"
description: 'The user''s identifier'
in: path in: path
name: userId name: userId
required: true required: true
@@ -1341,13 +1362,19 @@ paths:
type: array type: array
examples: examples:
'Example 1': 'Example 1':
value: [{groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users'}] value:
[
{
groupId: 123,
name: DCGroup,
description: 'This group represents Data Controller Users'
}
]
summary: 'Get list of all groups (groupName and groupDescription). All users can request this.' summary: 'Get list of all groups (groupName and groupDescription). All users can request this.'
tags: tags:
- Group - Group
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
post: post:
operationId: CreateGroup operationId: CreateGroup
@@ -1360,13 +1387,19 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} value:
{
groupId: 123,
name: DCGroup,
description: 'This group represents Data Controller Users',
isActive: true,
users: []
}
summary: 'Create a new group. Admin only.' summary: 'Create a new group. Admin only.'
tags: tags:
- Group - Group
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -1388,11 +1421,9 @@ paths:
tags: tags:
- Group - Group
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The group's name"
description: 'The group''s name'
in: path in: path
name: name name: name
required: true required: true
@@ -1412,11 +1443,9 @@ paths:
tags: tags:
- Group - Group
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The group's identifier"
description: 'The group''s identifier'
in: path in: path
name: groupId name: groupId
required: true required: true
@@ -1439,11 +1468,9 @@ paths:
tags: tags:
- Group - Group
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The group's identifier"
description: 'The group''s identifier'
in: path in: path
name: groupId name: groupId
required: true required: true
@@ -1463,16 +1490,21 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} value:
{
groupId: 123,
name: DCGroup,
description: 'This group represents Data Controller Users',
isActive: true,
users: []
}
summary: 'Add a user to a group. Admin task only.' summary: 'Add a user to a group. Admin task only.'
tags: tags:
- Group - Group
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The group's identifier"
description: 'The group''s identifier'
in: path in: path
name: groupId name: groupId
required: true required: true
@@ -1480,8 +1512,7 @@ paths:
format: double format: double
type: number type: number
example: '1234' example: '1234'
- - description: "The user's identifier"
description: 'The user''s identifier'
in: path in: path
name: userId name: userId
required: true required: true
@@ -1500,16 +1531,21 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} value:
{
groupId: 123,
name: DCGroup,
description: 'This group represents Data Controller Users',
isActive: true,
users: []
}
summary: 'Remove a user to a group. Admin task only.' summary: 'Remove a user to a group. Admin task only.'
tags: tags:
- Group - Group
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The group's identifier"
description: 'The group''s identifier'
in: path in: path
name: groupId name: groupId
required: true required: true
@@ -1517,8 +1553,7 @@ paths:
format: double format: double
type: number type: number
example: '1234' example: '1234'
- - description: "The user's identifier"
description: 'The user''s identifier'
in: path in: path
name: userId name: userId
required: true required: true
@@ -1538,7 +1573,14 @@ paths:
$ref: '#/components/schemas/InfoResponse' $ref: '#/components/schemas/InfoResponse'
examples: examples:
'Example 1': 'Example 1':
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http, runTimes: [sas, js]} value:
{
mode: desktop,
cors: enable,
whiteList: ['http://example.com', 'http://example2.com'],
protocol: http,
runTimes: [sas, js]
}
summary: 'Get server info (mode, cors, whiteList, protocol).' summary: 'Get server info (mode, cors, whiteList, protocol).'
tags: tags:
- Info - Info
@@ -1574,13 +1616,18 @@ paths:
$ref: '#/components/schemas/UserResponse' $ref: '#/components/schemas/UserResponse'
examples: examples:
'Example 1': 'Example 1':
value: {id: 123, username: johnusername, displayName: John, isAdmin: false} value:
{
id: 123,
username: johnusername,
displayName: John,
isAdmin: false
}
summary: 'Get session info (username).' summary: 'Get session info (username).'
tags: tags:
- Session - Session
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
/SASjsApi/stp/execute: /SASjsApi/stp/execute:
get: get:
@@ -1599,11 +1646,9 @@ paths:
tags: tags:
- STP - STP
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: 'Location of SAS or JS code'
description: 'Location of SAS or JS code'
in: query in: query
name: _program name: _program
required: true required: true
@@ -1621,17 +1666,25 @@ paths:
$ref: '#/components/schemas/ExecuteReturnJsonResponse' $ref: '#/components/schemas/ExecuteReturnJsonResponse'
examples: examples:
'Example 1': 'Example 1':
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}} value:
{
status: success,
_webout: 'webout content',
log: [],
httpHeaders:
{
Content-type: application/zip,
Cache-Control: 'public, max-age=1000'
}
}
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content." description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content."
summary: 'Execute a Stored Program, return a JSON object' summary: 'Execute a Stored Program, return a JSON object'
tags: tags:
- STP - STP
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: 'Location of SAS or JS code'
description: 'Location of SAS or JS code'
in: query in: query
name: _program name: _program
required: false required: false
@@ -1669,7 +1722,18 @@ paths:
application/json: application/json:
schema: schema:
properties: properties:
user: {properties: {isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [isAdmin, displayName, username, id], type: object} user:
{
properties:
{
isAdmin: { type: boolean },
displayName: { type: string },
username: { type: string },
id: { type: number, format: double }
},
required: [isAdmin, displayName, username, id],
type: object
}
loggedIn: { type: boolean } loggedIn: { type: boolean }
required: required:
- user - user
@@ -1738,13 +1802,39 @@ paths:
type: array type: array
examples: examples:
'Example 1': 'Example 1':
value: [{permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {permissionId: 124, uri: /SASjsApi/code/execute, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}] value:
[
{
permissionId: 123,
uri: /SASjsApi/code/execute,
setting: Grant,
user:
{
id: 1,
username: johnSnow01,
displayName: 'John Snow',
isAdmin: false
}
},
{
permissionId: 124,
uri: /SASjsApi/code/execute,
setting: Grant,
group:
{
groupId: 1,
name: DCGroup,
description: 'This group represents Data Controller Users',
isActive: true,
users: []
}
}
]
summary: 'Get list of all permissions (uri, setting and userDetail).' summary: 'Get list of all permissions (uri, setting and userDetail).'
tags: tags:
- Permission - Permission
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
post: post:
operationId: CreatePermission operationId: CreatePermission
@@ -1757,13 +1847,24 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse' $ref: '#/components/schemas/PermissionDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}} value:
{
permissionId: 123,
uri: /SASjsApi/code/execute,
setting: Grant,
user:
{
id: 1,
username: johnSnow01,
displayName: 'John Snow',
isAdmin: false
}
}
summary: 'Create a new permission. Admin only.' summary: 'Create a new permission. Admin only.'
tags: tags:
- Permission - Permission
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: [] parameters: []
requestBody: requestBody:
required: true required: true
@@ -1783,16 +1884,26 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse' $ref: '#/components/schemas/PermissionDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}} value:
{
permissionId: 123,
uri: /SASjsApi/code/execute,
setting: Grant,
user:
{
id: 1,
username: johnSnow01,
displayName: 'John Snow',
isAdmin: false
}
}
summary: 'Update permission setting. Admin only' summary: 'Update permission setting. Admin only'
tags: tags:
- Permission - Permission
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The permission's identifier"
description: 'The permission''s identifier'
in: path in: path
name: permissionId name: permissionId
required: true required: true
@@ -1815,11 +1926,9 @@ paths:
tags: tags:
- Permission - Permission
security: security:
- - bearerAuth: []
bearerAuth: []
parameters: parameters:
- - description: "The user's identifier"
description: 'The user''s identifier'
in: path in: path
name: permissionId name: permissionId
required: true required: true
@@ -1828,39 +1937,27 @@ paths:
type: number type: number
example: 1234 example: 1234
servers: servers:
- - url: /
url: /
tags: tags:
- - name: Auth
name: Auth
description: 'Operations about auth' description: 'Operations about auth'
- - name: Client
name: Client
description: 'Operations about clients' description: 'Operations about clients'
- - name: CODE
name: CODE
description: 'Execution of code (various runtimes are supported)' description: 'Execution of code (various runtimes are supported)'
- - name: Drive
name: Drive
description: 'Operations on SASjs Drive' description: 'Operations on SASjs Drive'
- - name: Group
name: Group
description: 'Operations on groups and group memberships' description: 'Operations on groups and group memberships'
- - name: Info
name: Info
description: 'Get Server Information' description: 'Get Server Information'
- - name: Permission
name: Permission
description: 'Operations about permissions' description: 'Operations about permissions'
- - name: Session
name: Session
description: 'Get Session information' description: 'Get Session information'
- - name: STP
name: STP
description: 'Execution of Stored Programs' description: 'Execution of Stored Programs'
- - name: User
name: User
description: 'Operations with users' description: 'Operations with users'
- - name: Web
name: Web
description: 'Operations on Web' description: 'Operations on Web'

View File

@@ -0,0 +1,21 @@
import { Express } from 'express'
import cors from 'cors'
import { CorsType } from '../utils'
export const configureCors = (app: Express) => {
const { CORS, WHITELIST } = process.env
if (CORS === CorsType.ENABLED) {
const whiteList: string[] = []
WHITELIST?.split(' ')
?.filter((url) => !!url)
.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
}

View File

@@ -0,0 +1,32 @@
import { Express } from 'express'
import mongoose from 'mongoose'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import { ModeType } from '../utils'
import { cookieOptions } from '../app'
export const configureExpressSession = (app: Express) => {
const { MODE } = process.env
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
if (process.env.NODE_ENV !== 'test') {
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
collectionName: 'sessions'
})
}
app.use(
session({
secret: process.secrets.SESSION_SECRET,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store,
cookie: cookieOptions
})
)
}
}

View File

@@ -0,0 +1,33 @@
import path from 'path'
import { Express } from 'express'
import morgan from 'morgan'
import { createStream } from 'rotating-file-stream'
import { generateTimestamp } from '@sasjs/utils'
import { getLogFolder } from '../utils'
export const configureLogger = (app: Express) => {
const { LOG_FORMAT_MORGAN } = process.env
let options
if (
process.env.NODE_ENV !== 'development' &&
process.env.NODE_ENV !== 'test'
) {
const timestamp = generateTimestamp()
const filename = `${timestamp}.log`
const logsFolder = getLogFolder()
// create a rotating write stream
var accessLogStream = createStream(filename, {
interval: '1d', // rotate daily
path: logsFolder
})
console.log('Writing Logs to :', path.join(logsFolder, filename))
options = { stream: accessLogStream }
}
// setup the logger
app.use(morgan(LOG_FORMAT_MORGAN as string, options))
}

View File

@@ -0,0 +1,26 @@
import { Express } from 'express'
import { getEnvCSPDirectives } from '../utils/parseHelmetConfig'
import { HelmetCoepType, ProtocolType } from '../utils'
import helmet from 'helmet'
export const configureSecurity = (app: Express) => {
const { PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } = process.env
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
HELMET_CSP_CONFIG_PATH
)
if (PROTOCOL === ProtocolType.HTTP)
cspConfigJson['upgrade-insecure-requests'] = null
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
...cspConfigJson
}
},
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
})
)
}

View File

@@ -0,0 +1,4 @@
export * from './configureCors'
export * from './configureExpressSession'
export * from './configureLogger'
export * from './configureSecurity'

View File

@@ -1,30 +1,26 @@
import path from 'path' import path from 'path'
import express, { ErrorRequestHandler } from 'express' import express, { ErrorRequestHandler } from 'express'
import mongoose from 'mongoose'
import csrf from 'csurf' import csrf from 'csurf'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import morgan from 'morgan'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import cors from 'cors'
import helmet from 'helmet'
import { import {
copySASjsCore, copySASjsCore,
CorsType,
getWebBuildFolder, getWebBuildFolder,
HelmetCoepType,
instantiateLogger, instantiateLogger,
loadAppStreamConfig, loadAppStreamConfig,
ModeType,
ProtocolType, ProtocolType,
ReturnCode, ReturnCode,
setProcessVariables, setProcessVariables,
setupFolders, setupFolders,
verifyEnvVariables verifyEnvVariables
} from './utils' } from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig' import {
configureCors,
configureExpressSession,
configureLogger,
configureSecurity
} from './app-modules'
dotenv.config() dotenv.config()
@@ -34,19 +30,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express() const app = express()
app.use(cookieParser()) const { PROTOCOL } = process.env
const {
MODE,
CORS,
WHITELIST,
PROTOCOL,
HELMET_CSP_CONFIG_PATH,
HELMET_COEP,
LOG_FORMAT_MORGAN
} = process.env
app.use(morgan(LOG_FORMAT_MORGAN as string))
export const cookieOptions = { export const cookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS, secure: PROTOCOL === ProtocolType.HTTPS,
@@ -54,79 +38,11 @@ export const cookieOptions = {
maxAge: 24 * 60 * 60 * 1000 // 24 hours maxAge: 24 * 60 * 60 * 1000 // 24 hours
} }
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
HELMET_CSP_CONFIG_PATH
)
if (PROTOCOL === ProtocolType.HTTP)
cspConfigJson['upgrade-insecure-requests'] = null
/*********************************** /***********************************
* CSRF Protection * * CSRF Protection *
***********************************/ ***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions }) export const csrfProtection = csrf({ cookie: cookieOptions })
/***********************************
* Handle security and origin *
***********************************/
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
...cspConfigJson
}
},
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
})
)
/***********************************
* Enabling CORS *
***********************************/
if (CORS === CorsType.ENABLED) {
const whiteList: string[] = []
WHITELIST?.split(' ')
?.filter((url) => !!url)
.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
export default setProcessVariables().then(async () => {
/***********************************
* DB Connection & *
* Express Sessions *
* With Mongo Store *
***********************************/
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
if (process.env.NODE_ENV !== 'test') {
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
collectionName: 'sessions'
})
}
app.use(
session({
secret: process.secrets.SESSION_SECRET,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store,
cookie: cookieOptions
})
)
}
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
const onError: ErrorRequestHandler = (err, req, res, next) => { const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!') return res.status(400).send('Invalid CSRF token!')
@@ -135,6 +51,31 @@ export default setProcessVariables().then(async () => {
res.status(500).send('Something broke!') res.status(500).send('Something broke!')
} }
export default setProcessVariables().then(async () => {
app.use(cookieParser())
configureLogger(app)
/***********************************
* Handle security and origin *
***********************************/
configureSecurity(app)
/***********************************
* Enabling CORS *
***********************************/
configureCors(app)
/***********************************
* DB Connection & *
* Express Sessions *
* With Mongo Store *
***********************************/
configureExpressSession(app)
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
await setupFolders() await setupFolders()
await copySASjsCore() await copySASjsCore()

View File

@@ -101,8 +101,8 @@ ${autoExecContent}`
session.path, session.path,
'-AUTOEXEC', '-AUTOEXEC',
autoExecPath, autoExecPath,
isWindows() ? '-nosplash' : '', process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
isWindows() ? '-icon' : '', process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
isWindows() ? '-nologo' : '' isWindows() ? '-nologo' : ''
]) ])
.then(() => { .then(() => {

View File

@@ -23,7 +23,9 @@ let _webout = '';
const weboutPath = '${ const weboutPath = '${
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
}'; }';
const _sasjs_tokenfile = '${tokenFile}'; const _sasjs_tokenfile = '${
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
}';
const _sasjs_username = '${preProgramVariables?.username}'; const _sasjs_username = '${preProgramVariables?.username}';
const _sasjs_userid = '${preProgramVariables?.userId}'; const _sasjs_userid = '${preProgramVariables?.userId}';
const _sasjs_displayname = '${preProgramVariables?.displayName}'; const _sasjs_displayname = '${preProgramVariables?.displayName}';

View File

@@ -3,6 +3,7 @@ declare namespace NodeJS {
sasLoc?: string sasLoc?: string
nodeLoc?: string nodeLoc?: string
driveLoc: string driveLoc: string
logsLoc: string
sasSessionController?: import('../../controllers/internal').SASSessionController sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController jsSessionController?: import('../../controllers/internal').JSSessionController
appStreamConfig: import('../').AppStreamConfig appStreamConfig: import('../').AppStreamConfig

View File

@@ -22,6 +22,8 @@ export const getDesktopUserAutoExecPath = () =>
export const getSasjsRootFolder = () => process.driveLoc export const getSasjsRootFolder = () => process.driveLoc
export const getLogFolder = () => process.logsLoc
export const getAppStreamConfigPath = () => export const getAppStreamConfigPath = () =>
path.join(getSasjsRootFolder(), 'appStreamConfig.json') path.join(getSasjsRootFolder(), 'appStreamConfig.json')
@@ -32,8 +34,6 @@ export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files') export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts') export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
export const getSessionsFolder = () => export const getSessionsFolder = () =>

View File

@@ -40,7 +40,16 @@ export const setProcessVariables = async () => {
await createFolder(absPath) await createFolder(absPath)
process.driveLoc = getRealPath(absPath) process.driveLoc = getRealPath(absPath)
const { LOG_LOCATION } = process.env
const absLogsPath = getAbsolutePath(
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
process.cwd()
)
await createFolder(absLogsPath)
process.logsLoc = getRealPath(absLogsPath)
console.log('sasLoc: ', process.sasLoc) console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc) console.log('sasDrive: ', process.driveLoc)
console.log('sasLogs: ', process.logsLoc)
console.log('runTimes: ', process.runTimes) console.log('runTimes: ', process.runTimes)
} }

View File

@@ -1,6 +1,6 @@
import path from 'path' import path from 'path'
import { MulterFile } from '../types/Upload' import { MulterFile } from '../types/Upload'
import { listFilesInFolder, readFileBinary } from '@sasjs/utils' import { listFilesInFolder, readFileBinary, isWindows } from '@sasjs/utils'
interface FilenameMapSingle { interface FilenameMapSingle {
fieldName: string fieldName: string
@@ -118,7 +118,9 @@ export const generateFileUploadJSCode = async (
if (fileName.includes('req_file')) { if (fileName.includes('req_file')) {
fileCount++ fileCount++
const filePath = path.join(sessionFolder, fileName) const filePath = path.join(sessionFolder, fileName)
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')` uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${
isWindows() ? filePath.replace(/\\/g, '\\\\') : filePath
}')`
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'` uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'` uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
} }