1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-20 04:10:06 +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)

View File

@@ -136,6 +136,9 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
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.
# Priority is given to the runtime that comes first in the string.
# 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
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",
"prestart": "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",
"build": "rimraf build && tsc",
"postbuild": "npm run copy:files",
@@ -63,6 +63,7 @@
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"url": "^0.10.3"

View File

@@ -85,10 +85,8 @@ components:
type: string
_webout:
anyOf:
-
type: string
-
$ref: '#/components/schemas/IRecordOfAny'
- type: string
- $ref: '#/components/schemas/IRecordOfAny'
log:
items:
$ref: '#/components/schemas/LogLine'
@@ -137,12 +135,9 @@ components:
members:
items:
anyOf:
-
$ref: '#/components/schemas/FolderMember'
-
$ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
- $ref: '#/components/schemas/FolderMember'
- $ref: '#/components/schemas/ServiceMember'
- $ref: '#/components/schemas/FileMember'
type: array
required:
- name
@@ -191,12 +186,9 @@ components:
members:
items:
anyOf:
-
$ref: '#/components/schemas/FolderMember'
-
$ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
- $ref: '#/components/schemas/FolderMember'
- $ref: '#/components/schemas/ServiceMember'
- $ref: '#/components/schemas/FileMember'
type: array
required:
- members
@@ -382,7 +374,7 @@ components:
autoExec:
type: string
description: 'User-specific auto-exec code'
example: ""
example: ''
required:
- displayName
- username
@@ -619,7 +611,11 @@ paths:
$ref: '#/components/schemas/TokenResponse'
examples:
'Example 1':
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString}
value:
{
accessToken: someRandomCryptoString,
refreshToken: someRandomCryptoString
}
summary: 'Accepts client/auth code and returns access/refresh tokens'
tags:
- Auth
@@ -643,13 +639,16 @@ paths:
$ref: '#/components/schemas/TokenResponse'
examples:
'Example 1':
value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString}
value:
{
accessToken: someRandomCryptoString,
refreshToken: someRandomCryptoString
}
summary: 'Returns new access/refresh tokens'
tags:
- Auth
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
/SASjsApi/auth/logout:
post:
@@ -661,8 +660,7 @@ paths:
tags:
- Auth
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
/SASjsApi/client:
post:
@@ -676,13 +674,16 @@ paths:
$ref: '#/components/schemas/ClientPayload'
examples:
'Example 1':
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
value:
{
clientId: someFormattedClientID1234,
clientSecret: someRandomCryptoString
}
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
tags:
- Client
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -705,8 +706,7 @@ paths:
tags:
- CODE
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -726,7 +726,11 @@ paths:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
value:
{
status: success,
message: 'Files deployed successfully to @sasjs/server.'
}
'400':
description: 'Invalid Format'
content:
@@ -735,7 +739,11 @@ paths:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Provided not supported data format.'}
value:
{
status: failure,
message: 'Provided not supported data format.'
}
'500':
description: 'Execution Error'
content:
@@ -744,13 +752,12 @@ paths:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Deployment failed!'}
value: { status: failure, message: 'Deployment failed!' }
summary: 'Creates/updates files within SASjs Drive using provided payload.'
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -770,7 +777,11 @@ paths:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
value:
{
status: success,
message: 'Files deployed successfully to @sasjs/server.'
}
'400':
description: 'Invalid Format'
content:
@@ -779,7 +790,11 @@ paths:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Provided not supported data format.'}
value:
{
status: failure,
message: 'Provided not supported data format.'
}
'500':
description: 'Execution Error'
content:
@@ -788,14 +803,13 @@ paths:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Deployment failed!'}
value: { status: failure, message: 'Deployment failed!' }
description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!"
summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -819,11 +833,9 @@ paths:
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
in: query
- in: query
name: _filePath
required: true
schema:
@@ -838,7 +850,7 @@ paths:
application/json:
schema:
properties:
status: {type: string}
status: { type: string }
required:
- status
type: object
@@ -846,11 +858,9 @@ paths:
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
in: query
- in: query
name: _filePath
required: true
schema:
@@ -867,7 +877,7 @@ paths:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: success}
value: { status: success }
'403':
description: 'File already exists'
content:
@@ -876,17 +886,15 @@ paths:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: failure, message: 'File request failed.'}
value: { status: failure, message: 'File request failed.' }
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Create a file in SASjs Drive'
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'Location of file'
- description: 'Location of file'
in: query
name: _filePath
required: false
@@ -918,26 +926,24 @@ paths:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: success}
value: { status: success }
'403':
description: ""
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: failure, message: 'File request failed.'}
value: { status: failure, message: 'File request failed.' }
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Modify a file in SASjs Drive'
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'Location of SAS program'
- description: 'Location of SAS program'
in: query
name: _filePath
required: false
@@ -968,8 +974,8 @@ paths:
application/json:
schema:
properties:
folders: {items: {type: string}, type: array}
files: {items: {type: string}, type: array}
folders: { items: { type: string }, type: array }
files: { items: { type: string }, type: array }
required:
- folders
- files
@@ -978,11 +984,9 @@ paths:
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
in: query
- in: query
name: _folderPath
required: false
schema:
@@ -997,7 +1001,7 @@ paths:
application/json:
schema:
properties:
status: {type: string}
status: { type: string }
required:
- status
type: object
@@ -1005,11 +1009,9 @@ paths:
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
in: query
- in: query
name: _folderPath
required: true
schema:
@@ -1026,7 +1028,7 @@ paths:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: success}
value: { status: success }
'409':
description: 'Folder already exists'
content:
@@ -1035,13 +1037,13 @@ paths:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'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'
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -1061,7 +1063,7 @@ paths:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: success}
value: { status: success }
'409':
description: 'Folder already exists'
content:
@@ -1070,13 +1072,12 @@ paths:
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: failure, message: 'rename request failed.'}
value: { status: failure, message: 'rename request failed.' }
summary: 'Renames a file/folder in SASjs Drive'
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -1098,8 +1099,7 @@ paths:
tags:
- Drive
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
/SASjsApi/user:
get:
@@ -1115,13 +1115,26 @@ paths:
type: array
examples:
'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.'
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
post:
operationId: CreateUser
@@ -1134,13 +1147,19 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'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.'
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -1163,11 +1182,9 @@ paths:
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The User''s username'
- description: "The User's username"
in: path
name: username
required: true
@@ -1185,16 +1202,21 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'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.'
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The User''s username'
- description: "The User's username"
in: path
name: username
required: true
@@ -1216,11 +1238,9 @@ paths:
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The User''s username'
- description: "The User's username"
in: path
name: username
required: true
@@ -1251,11 +1271,9 @@ paths:
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The user''s identifier'
- description: "The user's identifier"
in: path
name: userId
required: true
@@ -1274,16 +1292,21 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'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.'
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The user''s identifier'
- description: "The user's identifier"
in: path
name: userId
required: true
@@ -1306,11 +1329,9 @@ paths:
tags:
- User
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The user''s identifier'
- description: "The user's identifier"
in: path
name: userId
required: true
@@ -1341,13 +1362,19 @@ paths:
type: array
examples:
'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.'
tags:
- Group
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
post:
operationId: CreateGroup
@@ -1360,13 +1387,19 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'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.'
tags:
- Group
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -1388,11 +1421,9 @@ paths:
tags:
- Group
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The group''s name'
- description: "The group's name"
in: path
name: name
required: true
@@ -1412,11 +1443,9 @@ paths:
tags:
- Group
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The group''s identifier'
- description: "The group's identifier"
in: path
name: groupId
required: true
@@ -1433,17 +1462,15 @@ paths:
application/json:
schema:
allOf:
- {$ref: '#/components/schemas/IGroup'}
- {properties: {_id: {}}, required: [_id], type: object}
- { $ref: '#/components/schemas/IGroup' }
- { properties: { _id: {} }, required: [_id], type: object }
summary: 'Delete a group. Admin task only.'
tags:
- Group
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The group''s identifier'
- description: "The group's identifier"
in: path
name: groupId
required: true
@@ -1463,16 +1490,21 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'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.'
tags:
- Group
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The group''s identifier'
- description: "The group's identifier"
in: path
name: groupId
required: true
@@ -1480,8 +1512,7 @@ paths:
format: double
type: number
example: '1234'
-
description: 'The user''s identifier'
- description: "The user's identifier"
in: path
name: userId
required: true
@@ -1500,16 +1531,21 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'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.'
tags:
- Group
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The group''s identifier'
- description: "The group's identifier"
in: path
name: groupId
required: true
@@ -1517,8 +1553,7 @@ paths:
format: double
type: number
example: '1234'
-
description: 'The user''s identifier'
- description: "The user's identifier"
in: path
name: userId
required: true
@@ -1538,7 +1573,14 @@ paths:
$ref: '#/components/schemas/InfoResponse'
examples:
'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).'
tags:
- Info
@@ -1556,7 +1598,7 @@ paths:
$ref: '#/components/schemas/AuthorizedRoutesResponse'
examples:
'Example 1':
value: {URIs: [/AppStream, /SASjsApi/stp/execute]}
value: { URIs: [/AppStream, /SASjsApi/stp/execute] }
summary: 'Get authorized routes.'
tags:
- Info
@@ -1574,13 +1616,18 @@ paths:
$ref: '#/components/schemas/UserResponse'
examples:
'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).'
tags:
- Session
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
/SASjsApi/stp/execute:
get:
@@ -1592,18 +1639,16 @@ paths:
application/json:
schema:
anyOf:
- {type: string}
- {type: string, format: byte}
- { type: string }
- { type: string, format: byte }
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"
summary: 'Execute a Stored Program, returns raw _webout content.'
tags:
- STP
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'Location of SAS or JS code'
- description: 'Location of SAS or JS code'
in: query
name: _program
required: true
@@ -1621,17 +1666,25 @@ paths:
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
examples:
'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."
summary: 'Execute a Stored Program, return a JSON object'
tags:
- STP
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'Location of SAS or JS code'
- description: 'Location of SAS or JS code'
in: query
name: _program
required: false
@@ -1669,8 +1722,19 @@ paths:
application/json:
schema:
properties:
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}
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 }
required:
- user
- loggedIn
@@ -1698,7 +1762,7 @@ paths:
$ref: '#/components/schemas/AuthorizeResponse'
examples:
'Example 1':
value: {code: someRandomCryptoString}
value: { code: someRandomCryptoString }
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
tags:
- Web
@@ -1738,13 +1802,39 @@ paths:
type: array
examples:
'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).'
tags:
- Permission
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
post:
operationId: CreatePermission
@@ -1757,13 +1847,24 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse'
examples:
'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.'
tags:
- Permission
security:
-
bearerAuth: []
- bearerAuth: []
parameters: []
requestBody:
required: true
@@ -1783,16 +1884,26 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse'
examples:
'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'
tags:
- Permission
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The permission''s identifier'
- description: "The permission's identifier"
in: path
name: permissionId
required: true
@@ -1815,11 +1926,9 @@ paths:
tags:
- Permission
security:
-
bearerAuth: []
- bearerAuth: []
parameters:
-
description: 'The user''s identifier'
- description: "The user's identifier"
in: path
name: permissionId
required: true
@@ -1828,39 +1937,27 @@ paths:
type: number
example: 1234
servers:
-
url: /
- url: /
tags:
-
name: Auth
- name: Auth
description: 'Operations about auth'
-
name: Client
- name: Client
description: 'Operations about clients'
-
name: CODE
- name: CODE
description: 'Execution of code (various runtimes are supported)'
-
name: Drive
- name: Drive
description: 'Operations on SASjs Drive'
-
name: Group
- name: Group
description: 'Operations on groups and group memberships'
-
name: Info
- name: Info
description: 'Get Server Information'
-
name: Permission
- name: Permission
description: 'Operations about permissions'
-
name: Session
- name: Session
description: 'Get Session information'
-
name: STP
- name: STP
description: 'Execution of Stored Programs'
-
name: User
- name: User
description: 'Operations with users'
-
name: Web
- name: 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 express, { ErrorRequestHandler } from 'express'
import mongoose from 'mongoose'
import csrf from 'csurf'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import morgan from 'morgan'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import cors from 'cors'
import helmet from 'helmet'
import {
copySASjsCore,
CorsType,
getWebBuildFolder,
HelmetCoepType,
instantiateLogger,
loadAppStreamConfig,
ModeType,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders,
verifyEnvVariables
} from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
import {
configureCors,
configureExpressSession,
configureLogger,
configureSecurity
} from './app-modules'
dotenv.config()
@@ -34,19 +30,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
app.use(cookieParser())
const {
MODE,
CORS,
WHITELIST,
PROTOCOL,
HELMET_CSP_CONFIG_PATH,
HELMET_COEP,
LOG_FORMAT_MORGAN
} = process.env
app.use(morgan(LOG_FORMAT_MORGAN as string))
const { PROTOCOL } = process.env
export const cookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
@@ -54,86 +38,43 @@ export const cookieOptions = {
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 *
***********************************/
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')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack)
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 copySASjsCore()

View File

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

View File

@@ -23,7 +23,9 @@ let _webout = '';
const 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_userid = '${preProgramVariables?.userId}';
const _sasjs_displayname = '${preProgramVariables?.displayName}';

View File

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

View File

@@ -22,6 +22,8 @@ export const getDesktopUserAutoExecPath = () =>
export const getSasjsRootFolder = () => process.driveLoc
export const getLogFolder = () => process.logsLoc
export const getAppStreamConfigPath = () =>
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 getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
export const getSessionsFolder = () =>

View File

@@ -40,7 +40,16 @@ export const setProcessVariables = async () => {
await createFolder(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('sasDrive: ', process.driveLoc)
console.log('sasLogs: ', process.logsLoc)
console.log('runTimes: ', process.runTimes)
}

View File

@@ -1,6 +1,6 @@
import path from 'path'
import { MulterFile } from '../types/Upload'
import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
import { listFilesInFolder, readFileBinary, isWindows } from '@sasjs/utils'
interface FilenameMapSingle {
fieldName: string
@@ -118,7 +118,9 @@ export const generateFileUploadJSCode = async (
if (fileName.includes('req_file')) {
fileCount++
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_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
}