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

Compare commits

..

6 Commits

Author SHA1 Message Date
c43afabe28 chore: remove unused code 2023-08-08 15:07:00 +05:00
1531e9cd9c chore: addressed comments 2023-08-08 15:01:32 +05:00
8cdf605006 chore: fix specs 2023-05-10 17:02:13 +05:00
3f815e9beb chore: fix specs 2023-05-10 14:35:35 +05:00
6c88eeabd2 chore: specs fixed 2023-05-09 15:21:54 +05:00
093fe90589 feat: replace ID with UID
BREAKING CHANGE: remove auto incremental ids from user, group and permissions and add a virtual uid property that returns string value of documents object id
2023-05-09 15:01:56 +05:00
80 changed files with 10928 additions and 14039 deletions

View File

@@ -5,7 +5,7 @@ on:
jobs: jobs:
lint: lint:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
@@ -28,7 +28,7 @@ jobs:
run: npm run lint-web run: npm run lint-web
build-api: build-api:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
@@ -66,7 +66,7 @@ jobs:
CI: true CI: true
build-web: build-web:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:

View File

@@ -56,4 +56,4 @@ jobs:
- name: Release - name: Release
run: | run: |
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release

View File

@@ -1,3 +1,5 @@
{ {
"cSpell.words": ["autoexec", "initialising"] "cSpell.words": [
"autoexec"
]
} }

View File

@@ -1,87 +1,3 @@
## [0.39.1](https://github.com/sasjs/server/compare/v0.39.0...v0.39.1) (2025-03-13)
### Bug Fixes
* extra bit of sleep for file recognition ([f4768bf](https://github.com/sasjs/server/commit/f4768bffd3dbb2fe243966572ba74002024d96e1)), closes [#381](https://github.com/sasjs/server/issues/381)
# [0.39.0](https://github.com/sasjs/server/compare/v0.38.0...v0.39.0) (2024-10-31)
### Bug Fixes
* **api:** fixed condition in processProgram ([48a9a4d](https://github.com/sasjs/server/commit/48a9a4dd0e31f84209635382be4ec4bb2c3a9c0c))
### Features
* **api:** added session state endpoint ([6b6546c](https://github.com/sasjs/server/commit/6b6546c7ad0833347f8dc4cdba6ad19132f7aaef))
# [0.38.0](https://github.com/sasjs/server/compare/v0.37.0...v0.38.0) (2024-10-30)
### Features
* **api:** enabled query params in stp/trigger endpoint ([5cda9cd](https://github.com/sasjs/server/commit/5cda9cd5d8623b7ea2ecd989d7808f47ec866672))
# [0.37.0](https://github.com/sasjs/server/compare/v0.36.0...v0.37.0) (2024-10-29)
### Features
* **stp:** added trigger endpoint ([b0723f1](https://github.com/sasjs/server/commit/b0723f14448d60ffce4f2175cf8a73fc4d4dd0ee))
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)
### Features
* **code:** added code/trigger API endpoint ([ffcf193](https://github.com/sasjs/server/commit/ffcf193b87d811b166d79af74013776a253b50b0))
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)
### Bug Fixes
* **api:** fixed env issue in MacOS executable ([73d965d](https://github.com/sasjs/server/commit/73d965daf54b16c0921e4b18d11a1e6f8650884d))
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)
### Bug Fixes
* enable embedded LFs in JS STP vars ([7e8cbbf](https://github.com/sasjs/server/commit/7e8cbbf377b27a7f5dd9af0bc6605c01f302f5d9))
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)
### Bug Fixes
* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](https://github.com/sasjs/server/commit/9586dbb2d0d6611061c9efdfb84030144f62c2ee))
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)
### Bug Fixes
* **log-separator:** log separator should always wrap log ([8940f4d](https://github.com/sasjs/server/commit/8940f4dc47abae2036b4fcdeb772c31a0ca07cca))
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)
### Bug Fixes
* **editor:** fixed log/webout/print tabs ([d2de9dc](https://github.com/sasjs/server/commit/d2de9dc13ef2e980286dd03cca5e22cea443ed0c))
* **execute:** added atribute indicating stp api ([e78f87f](https://github.com/sasjs/server/commit/e78f87f5c00038ea11261dffb525ac8f1024e40b))
* **execute:** fixed adding print output ([9aaffce](https://github.com/sasjs/server/commit/9aaffce82051d81bf39adb69942bb321e9795141))
* **execution:** removed empty webout from response ([6dd2f4f](https://github.com/sasjs/server/commit/6dd2f4f87673336135bc7a6de0d2e143e192c025))
* **webout:** fixed adding empty webout to response payload ([31df72a](https://github.com/sasjs/server/commit/31df72ad88fe2c771d0ef8445d6db9dd147c40c9))
### Features
* **editor:** parse print output in response payload ([eb42683](https://github.com/sasjs/server/commit/eb42683fff701bd5b4d2b68760fe0c3ecad573dd))
## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01) ## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01)

View File

@@ -158,7 +158,7 @@ CORS=
WHITELIST= WHITELIST=
# HELMET Cross Origin Embedder Policy # HELMET Cross Origin Embedder Policy
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true` # Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# options: [true|false] default: true # options: [true|false] default: true
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`) # Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
HELMET_COEP= HELMET_COEP=

View File

@@ -25,7 +25,7 @@ LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron> LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
#default value is 100 #default value is 100
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100 MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
#default value is 10 #default value is 10
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10 MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10

7385
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -58,7 +58,7 @@
"express-session": "^1.17.2", "express-session": "^1.17.2",
"helmet": "^5.0.2", "helmet": "^5.0.2",
"joi": "^17.4.2", "joi": "^17.4.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^8.5.1",
"ldapjs": "2.3.3", "ldapjs": "2.3.3",
"mongoose": "^6.0.12", "mongoose": "^6.0.12",
"morgan": "^1.10.0", "morgan": "^1.10.0",
@@ -86,9 +86,9 @@
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5", "@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"axios": "^1.12.2", "axios": "0.27.2",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dotenv": "^16.0.1", "dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1", "http-headers-validation": "^0.0.1",
"jest": "^27.0.6", "jest": "^27.0.6",
"mongodb-memory-server": "8.11.4", "mongodb-memory-server": "8.11.4",

File diff suppressed because one or more lines are too long

View File

@@ -40,8 +40,7 @@ components:
clientId: clientId:
type: string type: string
userId: userId:
type: number type: string
format: double
required: required:
- clientId - clientId
- userId - userId
@@ -98,47 +97,17 @@ components:
properties: properties:
code: code:
type: string type: string
description: 'The code to be executed' description: 'Code of program'
example: '* Your Code HERE;' example: '* Code HERE;'
runTime: runTime:
$ref: '#/components/schemas/RunTimeType' $ref: '#/components/schemas/RunTimeType'
description: 'The runtime for the code - eg SAS, JS, PY or R' description: 'runtime for program'
example: js example: js
required: required:
- code - code
- runTime - runTime
type: object type: object
additionalProperties: false additionalProperties: false
TriggerCodeResponse:
properties:
sessionId:
type: string
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store code outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
example: 20241028074744-54132-1730101664824
required:
- sessionId
type: object
additionalProperties: false
TriggerCodePayload:
properties:
code:
type: string
description: 'The code to be executed'
example: '* Your Code HERE;'
runTime:
$ref: '#/components/schemas/RunTimeType'
description: 'The runtime for the code - eg SAS, JS, PY or R'
example: sas
expiresAfterMins:
type: number
format: double
description: "Amount of minutes after the completion of the job when the session must be\ndestroyed."
example: 15
required:
- code
- runTime
type: object
additionalProperties: false
MemberType.folder: MemberType.folder:
enum: enum:
- folder - folder
@@ -315,9 +284,8 @@ components:
additionalProperties: false additionalProperties: false
UserResponse: UserResponse:
properties: properties:
id: uid:
type: number type: string
format: double
username: username:
type: string type: string
displayName: displayName:
@@ -325,7 +293,7 @@ components:
isAdmin: isAdmin:
type: boolean type: boolean
required: required:
- id - uid
- username - username
- displayName - displayName
- isAdmin - isAdmin
@@ -333,32 +301,30 @@ components:
additionalProperties: false additionalProperties: false
GroupResponse: GroupResponse:
properties: properties:
groupId: uid:
type: number type: string
format: double
name: name:
type: string type: string
description: description:
type: string type: string
required: required:
- groupId - uid
- name - name
- description - description
type: object type: object
additionalProperties: false additionalProperties: false
UserDetailsResponse: UserDetailsResponse:
properties: properties:
id: uid:
type: number
format: double
displayName:
type: string type: string
username: username:
type: string type: string
isActive: displayName:
type: boolean type: string
isAdmin: isAdmin:
type: boolean type: boolean
isActive:
type: boolean
autoExec: autoExec:
type: string type: string
groups: groups:
@@ -366,11 +332,11 @@ components:
$ref: '#/components/schemas/GroupResponse' $ref: '#/components/schemas/GroupResponse'
type: array type: array
required: required:
- id - uid
- displayName
- username - username
- isActive - displayName
- isAdmin - isAdmin
- isActive
type: object type: object
additionalProperties: false additionalProperties: false
UserPayload: UserPayload:
@@ -406,9 +372,8 @@ components:
additionalProperties: false additionalProperties: false
GroupDetailsResponse: GroupDetailsResponse:
properties: properties:
groupId: uid:
type: number type: string
format: double
name: name:
type: string type: string
description: description:
@@ -420,7 +385,7 @@ components:
$ref: '#/components/schemas/UserResponse' $ref: '#/components/schemas/UserResponse'
type: array type: array
required: required:
- groupId - uid
- name - name
- description - description
- isActive - isActive
@@ -489,9 +454,8 @@ components:
additionalProperties: false additionalProperties: false
PermissionDetailsResponse: PermissionDetailsResponse:
properties: properties:
permissionId: uid:
type: number type: string
format: double
path: path:
type: string type: string
type: type:
@@ -503,7 +467,7 @@ components:
group: group:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
required: required:
- permissionId - uid
- path - path
- type - type
- setting - setting
@@ -542,10 +506,8 @@ components:
description: 'Indicates the type of principal' description: 'Indicates the type of principal'
example: user example: user
principalId: principalId:
type: number type: string
format: double
description: 'The id of user or group to which a rule is assigned.' description: 'The id of user or group to which a rule is assigned.'
example: 123
required: required:
- path - path
- type - type
@@ -564,35 +526,39 @@ components:
- setting - setting
type: object type: object
additionalProperties: false additionalProperties: false
SessionResponse: Pick_UserResponse.Exclude_keyofUserResponse.uid__:
properties: properties:
id:
type: number
format: double
username: username:
type: string type: string
displayName: displayName:
type: string type: string
isAdmin: isAdmin:
type: boolean type: boolean
needsToUpdatePassword:
type: boolean
required: required:
- id
- username - username
- displayName - displayName
- isAdmin - isAdmin
- needsToUpdatePassword type: object
description: 'From T, pick a set of properties whose keys are in the union K'
SessionResponse:
properties:
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
id:
type: string
needsToUpdatePassword:
type: boolean
required:
- username
- displayName
- isAdmin
- id
type: object type: object
additionalProperties: false additionalProperties: false
SessionState:
enum:
- initialising
- pending
- running
- completed
- failed
type: string
ExecutePostRequestPayload: ExecutePostRequestPayload:
properties: properties:
_program: _program:
@@ -601,16 +567,6 @@ components:
example: /Public/somefolder/some.file example: /Public/somefolder/some.file
type: object type: object
additionalProperties: false additionalProperties: false
TriggerProgramResponse:
properties:
sessionId:
type: string
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store program outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
example: 20241028074744-54132-1730101664824
required:
- sessionId
type: object
additionalProperties: false
LoginPayload: LoginPayload:
properties: properties:
username: username:
@@ -840,7 +796,7 @@ paths:
- {type: string} - {type: string}
- {type: string, format: byte} - {type: string, format: byte}
description: 'Execute Code on the Specified Runtime' description: 'Execute Code on the Specified Runtime'
summary: "Run Code and Return Webout Content, Log and Print output\nThe order of returned parts of the payload is:\n1. Webout (if present)\n2. Logs UUID (used as separator)\n3. Log\n4. Logs UUID (used as separator)\n5. Print (if present and if the runtime is SAS)\nPlease see" summary: 'Run Code and Return Webout Content and Log'
tags: tags:
- Code - Code
security: security:
@@ -853,30 +809,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteCodePayload' $ref: '#/components/schemas/ExecuteCodePayload'
/SASjsApi/code/trigger:
post:
operationId: TriggerCode
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodeResponse'
description: 'Trigger Code on the Specified Runtime'
summary: 'Triggers code and returns SessionId immediately - does not wait for job completion'
tags:
- Code
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodePayload'
/SASjsApi/drive/deploy: /SASjsApi/drive/deploy:
post: post:
operationId: Deploy operationId: Deploy
@@ -1278,7 +1210,7 @@ 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: [{uid: userIdString, username: johnusername, displayName: John, isAdmin: false}, {uid: anotherUserIdString, 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
@@ -1297,7 +1229,7 @@ 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: {uid: userIdString, 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
@@ -1348,7 +1280,7 @@ 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: {uid: userIdString, 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
@@ -1399,7 +1331,7 @@ paths:
password: password:
type: string type: string
type: object type: object
'/SASjsApi/user/{userId}': '/SASjsApi/user/{uid}':
get: get:
operationId: GetUser operationId: GetUser
responses: responses:
@@ -1418,14 +1350,12 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'The user''s identifier'
in: path in: path
name: userId name: uid
required: true required: true
schema: schema:
format: double type: string
type: number '/SASjsApi/user/{userId}':
example: 1234
patch: patch:
operationId: UpdateUser operationId: UpdateUser
responses: responses:
@@ -1437,7 +1367,7 @@ 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: {uid: userIdString, 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
@@ -1451,8 +1381,7 @@ paths:
name: userId name: userId
required: true required: true
schema: schema:
format: double type: string
type: number
example: '1234' example: '1234'
requestBody: requestBody:
required: true required: true
@@ -1478,8 +1407,7 @@ paths:
name: userId name: userId
required: true required: true
schema: schema:
format: double type: string
type: number
example: 1234 example: 1234
requestBody: requestBody:
required: true required: true
@@ -1504,7 +1432,7 @@ paths:
type: array type: array
examples: examples:
'Example 1': 'Example 1':
value: [{groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users'}] value: [{uid: groupIdString, 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
@@ -1523,7 +1451,7 @@ 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: {uid: groupIdString, 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
@@ -1539,7 +1467,7 @@ paths:
$ref: '#/components/schemas/GroupPayload' $ref: '#/components/schemas/GroupPayload'
'/SASjsApi/group/by/groupname/{name}': '/SASjsApi/group/by/groupname/{name}':
get: get:
operationId: GetGroupByGroupName operationId: GetGroupByName
responses: responses:
'200': '200':
description: Ok description: Ok
@@ -1561,7 +1489,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
'/SASjsApi/group/{groupId}': '/SASjsApi/group/{uid}':
get: get:
operationId: GetGroup operationId: GetGroup
responses: responses:
@@ -1581,12 +1509,11 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: groupId name: uid
required: true required: true
schema: schema:
format: double type: string
type: number example: 12ByteString
example: 1234
delete: delete:
operationId: DeleteGroup operationId: DeleteGroup
responses: responses:
@@ -1608,13 +1535,12 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: groupId name: uid
required: true required: true
schema: schema:
format: double type: string
type: number example: 12ByteString
example: 1234 '/SASjsApi/group/{groupUid}/{userUid}':
'/SASjsApi/group/{groupId}/{userId}':
post: post:
operationId: AddUserToGroup operationId: AddUserToGroup
responses: responses:
@@ -1626,7 +1552,7 @@ 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: {uid: groupIdString, 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
@@ -1637,21 +1563,18 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: groupId name: groupUid
required: true required: true
schema: schema:
format: double type: string
type: number example: 12ByteString
example: '1234'
- -
description: 'The user''s identifier' description: 'The user''s identifier'
in: path in: path
name: userId name: userUid
required: true required: true
schema: schema:
format: double type: string
type: number
example: '6789'
delete: delete:
operationId: RemoveUserFromGroup operationId: RemoveUserFromGroup
responses: responses:
@@ -1663,8 +1586,8 @@ 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: {uid: groupIdString, 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 from a group. Admin task only.'
tags: tags:
- Group - Group
security: security:
@@ -1674,21 +1597,19 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: groupId name: groupUid
required: true required: true
schema: schema:
format: double type: string
type: number example: 12ByteString
example: '1234'
- -
description: 'The user''s identifier' description: 'The user''s identifier'
in: path in: path
name: userId name: userUid
required: true required: true
schema: schema:
format: double type: string
type: number example: 12ByteString
example: '6789'
/SASjsApi/info: /SASjsApi/info:
get: get:
operationId: Info operationId: Info
@@ -1739,7 +1660,7 @@ paths:
type: array type: array
examples: examples:
'Example 1': 'Example 1':
value: [{permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {permissionId: 124, path: /SASjsApi/code/execute, type: Route, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}] value: [{uid: permissionId1String, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: user1-id, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {uid: permissionId2String, path: /SASjsApi/code/execute, type: Route, setting: Grant, group: {uid: group1-id, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}]
description: "Get the list of permission rules applicable the authenticated user.\nIf the user is an admin, all rules are returned." description: "Get the list of permission rules applicable the authenticated user.\nIf the user is an admin, all rules are returned."
summary: 'Get the list of permission rules. If the user is admin, all rules are returned.' summary: 'Get the list of permission rules. If the user is admin, all rules are returned.'
tags: tags:
@@ -1759,7 +1680,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse' $ref: '#/components/schemas/PermissionDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}} value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, 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
@@ -1773,7 +1694,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/RegisterPermissionPayload' $ref: '#/components/schemas/RegisterPermissionPayload'
'/SASjsApi/permission/{permissionId}': '/SASjsApi/permission/{uid}':
patch: patch:
operationId: UpdatePermission operationId: UpdatePermission
responses: responses:
@@ -1785,7 +1706,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse' $ref: '#/components/schemas/PermissionDetailsResponse'
examples: examples:
'Example 1': 'Example 1':
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}} value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
summary: 'Update permission setting. Admin only' summary: 'Update permission setting. Admin only'
tags: tags:
- Permission - Permission
@@ -1794,14 +1715,11 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'The permission''s identifier'
in: path in: path
name: permissionId name: uid
required: true required: true
schema: schema:
format: double type: string
type: number
example: 1234
requestBody: requestBody:
required: true required: true
content: content:
@@ -1821,14 +1739,11 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'The user''s identifier'
in: path in: path
name: permissionId name: uid
required: true required: true
schema: schema:
format: double type: string
type: number
example: 1234
/SASjsApi/session: /SASjsApi/session:
get: get:
operationId: Session operationId: Session
@@ -1841,7 +1756,7 @@ paths:
$ref: '#/components/schemas/SessionResponse' $ref: '#/components/schemas/SessionResponse'
examples: examples:
'Example 1': 'Example 1':
value: {id: 123, username: johnusername, displayName: John, isAdmin: false} value: {id: userIdString, username: johnusername, displayName: John, isAdmin: false, needsToUpdatePassword: false}
summary: 'Get session info (username).' summary: 'Get session info (username).'
tags: tags:
- Session - Session
@@ -1849,30 +1764,6 @@ paths:
- -
bearerAuth: [] bearerAuth: []
parameters: [] parameters: []
'/SASjsApi/session/{sessionId}/state':
get:
operationId: SessionState
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/SessionState'
description: "The polling endpoint is currently implemented for single-server deployments only.<br>\nLoad balanced / grid topologies will be supported in a future release.<br>\nIf your site requires this, please reach out to SASjs Support."
summary: 'Get session state (initialising, pending, running, completed, failed).'
tags:
- Session
security:
-
bearerAuth: []
parameters:
-
in: path
name: sessionId
required: true
schema:
type: string
/SASjsApi/stp/execute: /SASjsApi/stp/execute:
get: get:
operationId: ExecuteGetRequest operationId: ExecuteGetRequest
@@ -1885,7 +1776,7 @@ paths:
anyOf: anyOf:
- {type: string} - {type: string}
- {type: string, format: byte} - {type: string, format: byte}
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts additional URL parameters (converted to session variables)\nand file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms" description: "Trigger a Stored 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 _webout and (optionally) log.' summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
tags: tags:
- STP - STP
@@ -1894,22 +1785,13 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'Location of Stored Program in SASjs Drive.' description: 'Location of code in SASjs Drive'
in: query in: query
name: _program name: _program
required: true required: true
schema: schema:
type: string type: string
example: /Projects/myApp/some/program example: /Projects/myApp/some/program
-
description: 'Optional query param for setting debug mode (returns the session log in the response body).'
in: query
name: _debug
required: false
schema:
format: double
type: number
example: 131
post: post:
operationId: ExecutePostRequest operationId: ExecutePostRequest
responses: responses:
@@ -1943,50 +1825,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecutePostRequestPayload' $ref: '#/components/schemas/ExecutePostRequestPayload'
/SASjsApi/stp/trigger:
post:
operationId: TriggerProgram
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerProgramResponse'
description: 'Trigger Program on the Specified Runtime.'
summary: 'Triggers program and returns SessionId immediately - does not wait for program completion.'
tags:
- STP
security:
-
bearerAuth: []
parameters:
-
description: 'Location of code in SASjs Drive.'
in: query
name: _program
required: true
schema:
type: string
example: /Projects/myApp/some/program
-
description: 'Optional query param for setting debug mode.'
in: query
name: _debug
required: false
schema:
format: double
type: number
example: 131
-
description: 'Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.'
in: query
name: expiresAfterMins
required: false
schema:
format: double
type: number
example: 15
/: /:
get: get:
operationId: Home operationId: Home
@@ -2012,7 +1850,7 @@ paths:
application/json: application/json:
schema: schema:
properties: properties:
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object} user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
loggedIn: {type: boolean} loggedIn: {type: boolean}
required: required:
- user - user

View File

@@ -27,14 +27,14 @@ import User from '../model/User'
@Tags('Auth') @Tags('Auth')
export class AuthController { export class AuthController {
static authCodes: { [key: string]: { [key: string]: string } } = {} static authCodes: { [key: string]: { [key: string]: string } } = {}
static saveCode = (userId: number, clientId: string, code: string) => { static saveCode = (userId: string, clientId: string, code: string) => {
if (AuthController.authCodes[userId]) if (AuthController.authCodes[userId])
return (AuthController.authCodes[userId][clientId] = code) return (AuthController.authCodes[userId][clientId] = code)
AuthController.authCodes[userId] = { [clientId]: code } AuthController.authCodes[userId] = { [clientId]: code }
return AuthController.authCodes[userId][clientId] return AuthController.authCodes[userId][clientId]
} }
static deleteCode = (userId: number, clientId: string) => static deleteCode = (userId: string, clientId: string) =>
delete AuthController.authCodes[userId][clientId] delete AuthController.authCodes[userId][clientId]
/** /**
@@ -159,7 +159,7 @@ const updatePassword = async (
) => { ) => {
const { currentPassword, newPassword } = data const { currentPassword, newPassword } = data
const userId = req.user?.userId const userId = req.user?.userId
const dbUser = await User.findOne({ id: userId }) const dbUser = await User.findOne({ _id: userId })
if (!dbUser) if (!dbUser)
throw { throw {

View File

@@ -1,71 +1,34 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa' import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController, getSessionController } from './internal' import { ExecutionController } from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
getUserAutoExec, getUserAutoExec,
ModeType, ModeType,
parseLogToArray,
RunTimeType RunTimeType
} from '../utils' } from '../utils'
interface ExecuteCodePayload { interface ExecuteCodePayload {
/** /**
* The code to be executed * Code of program
* @example "* Your Code HERE;" * @example "* Code HERE;"
*/ */
code: string code: string
/** /**
* The runtime for the code - eg SAS, JS, PY or R * runtime for program
* @example "js" * @example "js"
*/ */
runTime: RunTimeType runTime: RunTimeType
} }
interface TriggerCodePayload {
/**
* The code to be executed
* @example "* Your Code HERE;"
*/
code: string
/**
* The runtime for the code - eg SAS, JS, PY or R
* @example "sas"
*/
runTime: RunTimeType
/**
* Amount of minutes after the completion of the job when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
}
interface TriggerCodeResponse {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store code outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/code') @Route('SASjsApi/code')
@Tags('Code') @Tags('Code')
export class CodeController { export class CodeController {
/** /**
* Execute Code on the Specified Runtime * Execute Code on the Specified Runtime
* @summary Run Code and Return Webout Content, Log and Print output * @summary Run Code and Return Webout Content and Log
* The order of returned parts of the payload is:
* 1. Webout (if present)
* 2. Logs UUID (used as separator)
* 3. Log
* 4. Logs UUID (used as separator)
* 5. Print (if present and if the runtime is SAS)
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
*/ */
@Post('/execute') @Post('/execute')
public async executeCode( public async executeCode(
@@ -74,18 +37,6 @@ export class CodeController {
): Promise<string | Buffer> { ): Promise<string | Buffer> {
return executeCode(request, body) return executeCode(request, body)
} }
/**
* Trigger Code on the Specified Runtime
* @summary Triggers code and returns SessionId immediately - does not wait for job completion
*/
@Post('/trigger')
public async triggerCode(
@Request() request: express.Request,
@Body() body: TriggerCodePayload
): Promise<TriggerCodeResponse> {
return triggerCode(request, body)
}
} }
const executeCode = async ( const executeCode = async (
@@ -104,8 +55,7 @@ const executeCode = async (
preProgramVariables: getPreProgramVariables(req), preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 }, vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec }, otherArgs: { userAutoExec },
runTime: runTime, runTime: runTime
includePrintOutput: true
}) })
return result return result
@@ -118,49 +68,3 @@ const executeCode = async (
} }
} }
} }
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<TriggerCodeResponse> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
try {
// call executeProgram method of ExecutionController without awaiting
new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true,
session // session is provided
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -12,28 +12,29 @@ import {
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group' import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User' import User from '../model/User'
import { AuthProviderType } from '../utils' import { GetUserBy, UserResponse } from './user'
import { UserResponse } from './user'
export interface GroupResponse { export interface GroupResponse {
groupId: number uid: string
name: string name: string
description: string description: string
} }
export interface GroupDetailsResponse { export interface GroupDetailsResponse extends GroupResponse {
groupId: number
name: string
description: string
isActive: boolean isActive: boolean
users: UserResponse[] users: UserResponse[]
} }
interface GetGroupBy { interface GetGroupBy {
groupId?: number _id?: string
name?: string name?: string
} }
enum GroupAction {
AddUser = 'addUser',
RemoveUser = 'removeUser'
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/group') @Route('SASjsApi/group')
@Tags('Group') @Tags('Group')
@@ -44,7 +45,7 @@ export class GroupController {
*/ */
@Example<GroupResponse[]>([ @Example<GroupResponse[]>([
{ {
groupId: 123, uid: 'groupIdString',
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users' description: 'This group represents Data Controller Users'
} }
@@ -59,7 +60,7 @@ export class GroupController {
* *
*/ */
@Example<GroupDetailsResponse>({ @Example<GroupDetailsResponse>({
groupId: 123, uid: 'groupIdString',
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
@@ -78,7 +79,7 @@ export class GroupController {
* @example dcgroup * @example dcgroup
*/ */
@Get('by/groupname/{name}') @Get('by/groupname/{name}')
public async getGroupByGroupName( public async getGroupByName(
@Path() name: string @Path() name: string
): Promise<GroupDetailsResponse> { ): Promise<GroupDetailsResponse> {
return getGroup({ name }) return getGroup({ name })
@@ -86,68 +87,66 @@ export class GroupController {
/** /**
* @summary Get list of members of a group (userName). All users can request this. * @summary Get list of members of a group (userName). All users can request this.
* @param groupId The group's identifier * @param uid The group's identifier
* @example groupId 1234 * @example uid "12ByteString"
*/ */
@Get('{groupId}') @Get('{uid}')
public async getGroup( public async getGroup(@Path() uid: string): Promise<GroupDetailsResponse> {
@Path() groupId: number return getGroup({ _id: uid })
): Promise<GroupDetailsResponse> {
return getGroup({ groupId })
} }
/** /**
* @summary Add a user to a group. Admin task only. * @summary Add a user to a group. Admin task only.
* @param groupId The group's identifier * @param groupUid The group's identifier
* @example groupId "1234" * @example groupUid "12ByteString"
* @param userId The user's identifier * @param userUid The user's identifier
* @example userId "6789" * @example userId "12ByteString"
*/ */
@Example<GroupDetailsResponse>({ @Example<GroupDetailsResponse>({
groupId: 123, uid: 'groupIdString',
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
users: [] users: []
}) })
@Post('{groupId}/{userId}') @Post('{groupUid}/{userUid}')
public async addUserToGroup( public async addUserToGroup(
@Path() groupId: number, @Path() groupUid: string,
@Path() userId: number @Path() userUid: string
): Promise<GroupDetailsResponse> { ): Promise<GroupDetailsResponse> {
return addUserToGroup(groupId, userId) return addUserToGroup(groupUid, userUid)
} }
/** /**
* @summary Remove a user to a group. Admin task only. * @summary Remove a user from a group. Admin task only.
* @param groupId The group's identifier * @param groupUid The group's identifier
* @example groupId "1234" * @example groupUid "12ByteString"
* @param userId The user's identifier * @param userUid The user's identifier
* @example userId "6789" * @example userUid "12ByteString"
*/ */
@Example<GroupDetailsResponse>({ @Example<GroupDetailsResponse>({
groupId: 123, uid: 'groupIdString',
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
users: [] users: []
}) })
@Delete('{groupId}/{userId}') @Delete('{groupUid}/{userUid}')
public async removeUserFromGroup( public async removeUserFromGroup(
@Path() groupId: number, @Path() groupUid: string,
@Path() userId: number @Path() userUid: string
): Promise<GroupDetailsResponse> { ): Promise<GroupDetailsResponse> {
return removeUserFromGroup(groupId, userId) return removeUserFromGroup(groupUid, userUid)
} }
/** /**
* @summary Delete a group. Admin task only. * @summary Delete a group. Admin task only.
* @param groupId The group's identifier * @param uid The group's identifier
* @example groupId 1234 * @example uid "12ByteString"
*/ */
@Delete('{groupId}') @Delete('{uid}')
public async deleteGroup(@Path() groupId: number) { public async deleteGroup(@Path() uid: string) {
const group = await Group.findOne({ groupId }) const group = await Group.findOne({ _id: uid })
if (!group) if (!group)
throw { throw {
code: 404, code: 404,
@@ -160,9 +159,7 @@ export class GroupController {
} }
const getAllGroups = async (): Promise<GroupResponse[]> => const getAllGroups = async (): Promise<GroupResponse[]> =>
await Group.find({}) await Group.find({}).select('uid name description').exec()
.select({ _id: 0, groupId: 1, name: 1, description: 1 })
.exec()
const createGroup = async ({ const createGroup = async ({
name, name,
@@ -187,7 +184,7 @@ const createGroup = async ({
const savedGroup = await group.save() const savedGroup = await group.save()
return { return {
groupId: savedGroup.groupId, uid: savedGroup.uid,
name: savedGroup.name, name: savedGroup.name,
description: savedGroup.description, description: savedGroup.description,
isActive: savedGroup.isActive, isActive: savedGroup.isActive,
@@ -198,11 +195,12 @@ const createGroup = async ({
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => { const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne( const group = (await Group.findOne(
findBy, findBy,
'groupId name description isActive users -_id' 'uid name description isActive users'
).populate( ).populate(
'users', 'users',
'id username displayName isAdmin -_id' 'uid username displayName isAdmin'
)) as unknown as GroupDetailsResponse )) as unknown as GroupDetailsResponse
if (!group) if (!group)
throw { throw {
code: 404, code: 404,
@@ -211,7 +209,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
} }
return { return {
groupId: group.groupId, uid: group.uid,
name: group.name, name: group.name,
description: group.description, description: group.description,
isActive: group.isActive, isActive: group.isActive,
@@ -220,23 +218,23 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
} }
const addUserToGroup = async ( const addUserToGroup = async (
groupId: number, groupUid: string,
userId: number userUid: string
): Promise<GroupDetailsResponse> => ): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupId, userId, 'addUser') updateUsersListInGroup(groupUid, userUid, GroupAction.AddUser)
const removeUserFromGroup = async ( const removeUserFromGroup = async (
groupId: number, groupUid: string,
userId: number userUid: string
): Promise<GroupDetailsResponse> => ): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupId, userId, 'removeUser') updateUsersListInGroup(groupUid, userUid, GroupAction.RemoveUser)
const updateUsersListInGroup = async ( const updateUsersListInGroup = async (
groupId: number, groupUid: string,
userId: number, userUid: string,
action: 'addUser' | 'removeUser' action: GroupAction
): Promise<GroupDetailsResponse> => { ): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ groupId }) const group = await Group.findOne({ _id: groupUid })
if (!group) if (!group)
throw { throw {
code: 404, code: 404,
@@ -258,7 +256,7 @@ const updateUsersListInGroup = async (
message: `Can't add/remove user to group created by external auth provider.` message: `Can't add/remove user to group created by external auth provider.`
} }
const user = await User.findOne({ id: userId }) const user = await User.findOne({ _id: userUid })
if (!user) if (!user)
throw { throw {
code: 404, code: 404,
@@ -274,7 +272,7 @@ const updateUsersListInGroup = async (
} }
const updatedGroup = const updatedGroup =
action === 'addUser' action === GroupAction.AddUser
? await group.addUser(user) ? await group.addUser(user)
: await group.removeUser(user) : await group.removeUser(user)
@@ -286,7 +284,7 @@ const updateUsersListInGroup = async (
} }
return { return {
groupId: updatedGroup.groupId, uid: updatedGroup.uid,
name: updatedGroup.name, name: updatedGroup.name,
description: updatedGroup.description, description: updatedGroup.description,
isActive: updatedGroup.isActive, isActive: updatedGroup.isActive,

View File

@@ -2,7 +2,7 @@ import path from 'path'
import fs from 'fs' import fs from 'fs'
import { getSessionController, processProgram } from './' import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils' import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode, SessionState } from '../../types' import { PreProgramVars, Session, TreeNode } from '../../types'
import { import {
extractHeaders, extractHeaders,
getFilesFolder, getFilesFolder,
@@ -33,7 +33,6 @@ interface ExecuteFileParams {
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> { interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string program: string
includePrintOutput?: boolean
} }
export class ExecutionController { export class ExecutionController {
@@ -68,17 +67,18 @@ export class ExecutionController {
otherArgs, otherArgs,
session: sessionByFileUpload, session: sessionByFileUpload,
runTime, runTime,
forceStringResult, forceStringResult
includePrintOutput
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> { }: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime) const sessionController = getSessionController(runTime)
const session = const session =
sessionByFileUpload ?? (await sessionController.getSession()) sessionByFileUpload ?? (await sessionController.getSession())
session.state = SessionState.running session.inUse = true
session.consumed = true
const logPath = path.join(session.path, 'log.log') const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt') const headersPath = path.join(session.path, 'stpsrv_header.txt')
const weboutPath = path.join(session.path, 'webout.txt') const weboutPath = path.join(session.path, 'webout.txt')
const tokenFile = path.join(session.path, 'reqHeaders.txt') const tokenFile = path.join(session.path, 'reqHeaders.txt')
@@ -120,32 +120,13 @@ export class ExecutionController {
: '' : ''
// it should be deleted by scheduleSessionDestroy // it should be deleted by scheduleSessionDestroy
session.state = SessionState.completed session.inUse = false
const resultParts = []
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty
if (webout && webout.length !== 0) resultParts.push(webout)
// INFO: log separator wraps the log from the beginning and the end
resultParts.push(process.logsUUID)
resultParts.push(log)
resultParts.push(process.logsUUID)
if (includePrintOutput && runTime === RunTimeType.SAS) {
const printOutputPath = path.join(session.path, 'output.lst')
const printOutput = (await fileExists(printOutputPath))
? await readFile(printOutputPath)
: ''
if (printOutput) resultParts.push(printOutput)
}
return { return {
httpHeaders, httpHeaders,
result: result:
isDebugOn(vars) || session.failureReason isDebugOn(vars) || session.crashed
? resultParts.join(`\n`) ? `${webout}\n${process.logsUUID}\n${log}`
: webout : webout
} }
} }

View File

@@ -2,8 +2,11 @@ import { Request, RequestHandler } from 'express'
import multer from 'multer' import multer from 'multer'
import { uuidv4 } from '@sasjs/utils' import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.' import { getSessionController } from '.'
import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils' import {
import { SessionState } from '../../types' executeProgramRawValidation,
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
export class FileUploadController { export class FileUploadController {
private storage = multer.diskStorage({ private storage = multer.diskStorage({
@@ -53,8 +56,9 @@ export class FileUploadController {
} }
const session = await sessionController.getSession() const session = await sessionController.getSession()
// change session state to 'running', so that it's not available for any other request // marking consumed true, so that it's not available
session.state = SessionState.running // as readySession for any other request
session.consumed = true
req.sasjsSession = session req.sasjsSession = session

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import { Session, SessionState } from '../../types' import { Session } from '../../types'
import { promisify } from 'util' import { promisify } from 'util'
import { execFile } from 'child_process' import { execFile } from 'child_process'
import { import {
@@ -14,7 +14,8 @@ import {
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile readFile,
isWindows
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -23,9 +24,7 @@ export class SessionController {
protected sessions: Session[] = [] protected sessions: Session[] = []
protected getReadySessions = (): Session[] => protected getReadySessions = (): Session[] =>
this.sessions.filter( this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
(session: Session) => session.state === SessionState.pending
)
protected async createSession(): Promise<Session> { protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp()) const sessionId = generateUniqueFileName(generateTimestamp())
@@ -41,18 +40,19 @@ export class SessionController {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
state: SessionState.pending, ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder
} }
const headersPath = path.join(session.path, 'stpsrv_header.txt') const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8') await createFile(headersPath, 'content-type: text/html; charset=utf-8')
this.sessions.push(session) this.sessions.push(session)
return session return session
} }
@@ -67,10 +67,6 @@ export class SessionController {
return session return session
} }
public getSessionById(id: string) {
return this.sessions.find((session) => session.id === id)
}
} }
export class SASSessionController extends SessionController { export class SASSessionController extends SessionController {
@@ -88,7 +84,10 @@ export class SASSessionController extends SessionController {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
state: SessionState.initialising, ready: false,
inUse: false,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder
@@ -146,20 +145,13 @@ ${autoExecContent}`
process.sasLoc!.endsWith('sas.exe') ? session.path : '' process.sasLoc!.endsWith('sas.exe') ? session.path : ''
]) ])
.then(() => { .then(() => {
session.state = SessionState.completed session.completed = true
process.logger.info('session completed', session) process.logger.info('session completed', session)
}) })
.catch((err) => { .catch((err) => {
session.state = SessionState.failed session.completed = true
session.crashed = err.toString()
session.failureReason = err.toString() process.logger.error('session crashed', session.id, session.crashed)
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
}) })
// we have a triggered session - add to array // we have a triggered session - add to array
@@ -176,19 +168,15 @@ ${autoExecContent}`
const codeFilePath = path.join(session.path, 'code.sas') const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever // TODO: don't wait forever
while ( while ((await fileExists(codeFilePath)) && !session.crashed) {}
(await fileExists(codeFilePath)) &&
session.state !== SessionState.failed
) {}
if (session.state === SessionState.failed) { if (session.crashed)
process.logger.error( process.logger.error(
'session crashed! while waiting to be ready', 'session crashed! while waiting to be ready',
session.failureReason session.crashed
) )
} else {
session.state = SessionState.pending session.ready = true
}
} }
private async deleteSession(session: Session) { private async deleteSession(session: Session) {
@@ -202,37 +190,17 @@ ${autoExecContent}`
} }
private scheduleSessionDestroy(session: Session) { private scheduleSessionDestroy(session: Session) {
setTimeout( setTimeout(async () => {
async () => { if (session.inUse) {
if (session.state === SessionState.running) { // adding 10 more minutes
// adding 10 more minutes const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
const newDeathTimeStamp = session.deathTimeStamp = newDeathTimeStamp.toString()
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session) this.scheduleSessionDestroy(session)
} else { } else {
const { expiresAfterMins } = session await this.deleteSession(session)
}
// delay session destroy if expiresAfterMins present }, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
if (expiresAfterMins && session.state !== SessionState.completed) {
// calculate session death time using expiresAfterMins
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) +
expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}
},
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
)
} }
} }
@@ -260,16 +228,9 @@ data _null_;
rc=filename(fname,getoption('SYSIN') ); rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname); if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname); rc=filename(fname);
/* now wait for the real SYSIN (location of code.sas) */ /* now wait for the real SYSIN */
slept=0;fname=''; slept=0;
do until (slept>(60*15)); do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
rc=filename(fname,getoption('SYSIN'));
if rc = 0 and fexist(fname) then do;
putlog fname=;
rc=filename(fname);
rc=sleep(0.01,1); /* wait just a little more */
stop;
end;
slept=slept+sleep(0.01,1); slept=slept+sleep(0.01,1);
end; end;
stop; stop;

View File

@@ -15,7 +15,7 @@ export const createJSProgram = async (
) => { ) => {
const varStatments = Object.keys(vars).reduce( const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => (computed: string, key: string) =>
`${computed}const ${key} = \`${vars[key]}\`;\n`, `${computed}const ${key} = '${vars[key]}';\n`,
'' ''
) )

View File

@@ -3,7 +3,7 @@ import { WriteStream, createWriteStream } from 'fs'
import { execFile } from 'child_process' import { execFile } from 'child_process'
import { once } from 'stream' import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils' import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session, SessionState } from '../../types' import { PreProgramVars, Session } from '../../types'
import { RunTimeType } from '../../utils' import { RunTimeType } from '../../utils'
import { import {
ExecutionVars, ExecutionVars,
@@ -49,7 +49,7 @@ export const processProgram = async (
await moveFile(codePath + '.bkp', codePath) await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status // we now need to poll the session status
while (session.state !== SessionState.completed) { while (!session.completed) {
await delay(50) await delay(50)
} }
} else { } else {
@@ -114,20 +114,13 @@ export const processProgram = async (
await execFilePromise(executablePath, [codePath], writeStream) await execFilePromise(executablePath, [codePath], writeStream)
.then(() => { .then(() => {
session.state = SessionState.completed session.completed = true
process.logger.info('session completed', session) process.logger.info('session completed', session)
}) })
.catch((err) => { .catch((err) => {
session.state = SessionState.failed session.completed = true
session.crashed = err.toString()
session.failureReason = err.toString() process.logger.error('session crashed', session.id, session.crashed)
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
}) })
// copy the code file to log and end write stream // copy the code file to log and end write stream

View File

@@ -56,9 +56,9 @@ interface RegisterPermissionPayload {
principalType: PrincipalType principalType: PrincipalType
/** /**
* The id of user or group to which a rule is assigned. * The id of user or group to which a rule is assigned.
* @example 123 * @example 'groupIdString'
*/ */
principalId: number principalId: string
} }
interface UpdatePermissionPayload { interface UpdatePermissionPayload {
@@ -70,7 +70,7 @@ interface UpdatePermissionPayload {
} }
export interface PermissionDetailsResponse { export interface PermissionDetailsResponse {
permissionId: number uid: string
path: string path: string
type: string type: string
setting: string setting: string
@@ -91,24 +91,24 @@ export class PermissionController {
*/ */
@Example<PermissionDetailsResponse[]>([ @Example<PermissionDetailsResponse[]>([
{ {
permissionId: 123, uid: 'permissionId1String',
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
user: { user: {
id: 1, uid: 'user1-id',
username: 'johnSnow01', username: 'johnSnow01',
displayName: 'John Snow', displayName: 'John Snow',
isAdmin: false isAdmin: false
} }
}, },
{ {
permissionId: 124, uid: 'permissionId2String',
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
group: { group: {
groupId: 1, uid: 'group1-id',
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
@@ -128,12 +128,12 @@ export class PermissionController {
* *
*/ */
@Example<PermissionDetailsResponse>({ @Example<PermissionDetailsResponse>({
permissionId: 123, uid: 'permissionIdString',
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
user: { user: {
id: 1, uid: 'userIdString',
username: 'johnSnow01', username: 'johnSnow01',
displayName: 'John Snow', displayName: 'John Snow',
isAdmin: false isAdmin: false
@@ -149,36 +149,36 @@ export class PermissionController {
/** /**
* @summary Update permission setting. Admin only * @summary Update permission setting. Admin only
* @param permissionId The permission's identifier * @param permissionId The permission's identifier
* @example permissionId 1234 * @example permissionId "permissionIdString"
*/ */
@Example<PermissionDetailsResponse>({ @Example<PermissionDetailsResponse>({
permissionId: 123, uid: 'permissionIdString',
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
user: { user: {
id: 1, uid: 'userIdString',
username: 'johnSnow01', username: 'johnSnow01',
displayName: 'John Snow', displayName: 'John Snow',
isAdmin: false isAdmin: false
} }
}) })
@Patch('{permissionId}') @Patch('{uid}')
public async updatePermission( public async updatePermission(
@Path() permissionId: number, @Path() uid: string,
@Body() body: UpdatePermissionPayload @Body() body: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> { ): Promise<PermissionDetailsResponse> {
return updatePermission(permissionId, body) return updatePermission(uid, body)
} }
/** /**
* @summary Delete a permission. Admin only. * @summary Delete a permission. Admin only.
* @param permissionId The user's identifier * @param permissionId The user's identifier
* @example permissionId 1234 * @example permissionId "permissionIdString"
*/ */
@Delete('{permissionId}') @Delete('{uid}')
public async deletePermission(@Path() permissionId: number) { public async deletePermission(@Path() uid: string) {
return deletePermission(permissionId) return deletePermission(uid)
} }
} }
@@ -191,7 +191,7 @@ const getAllPermissions = async (
else { else {
const permissions: PermissionDetailsResponse[] = [] const permissions: PermissionDetailsResponse[] = []
const dbUser = await User.findOne({ id: user?.userId }) const dbUser = await User.findOne({ _id: user?.userId })
if (!dbUser) if (!dbUser)
throw { throw {
code: 404, code: 404,
@@ -227,7 +227,7 @@ const createPermission = async ({
switch (principalType) { switch (principalType) {
case PrincipalType.user: { case PrincipalType.user: {
const userInDB = await User.findOne({ id: principalId }) const userInDB = await User.findOne({ _id: principalId })
if (!userInDB) if (!userInDB)
throw { throw {
code: 404, code: 404,
@@ -259,7 +259,7 @@ const createPermission = async ({
permission.user = userInDB._id permission.user = userInDB._id
user = { user = {
id: userInDB.id, uid: userInDB.uid,
username: userInDB.username, username: userInDB.username,
displayName: userInDB.displayName, displayName: userInDB.displayName,
isAdmin: userInDB.isAdmin isAdmin: userInDB.isAdmin
@@ -267,7 +267,7 @@ const createPermission = async ({
break break
} }
case PrincipalType.group: { case PrincipalType.group: {
const groupInDB = await Group.findOne({ groupId: principalId }) const groupInDB = await Group.findOne({ _id: principalId })
if (!groupInDB) if (!groupInDB)
throw { throw {
code: 404, code: 404,
@@ -291,13 +291,13 @@ const createPermission = async ({
permission.group = groupInDB._id permission.group = groupInDB._id
group = { group = {
groupId: groupInDB.groupId, uid: groupInDB.uid,
name: groupInDB.name, name: groupInDB.name,
description: groupInDB.description, description: groupInDB.description,
isActive: groupInDB.isActive, isActive: groupInDB.isActive,
users: groupInDB.populate({ users: groupInDB.populate({
path: 'users', path: 'users',
select: 'id username displayName isAdmin -_id', select: 'uid username displayName isAdmin -_id',
options: { limit: 15 } options: { limit: 15 }
}) as unknown as UserResponse[] }) as unknown as UserResponse[]
} }
@@ -314,7 +314,7 @@ const createPermission = async ({
const savedPermission = await permission.save() const savedPermission = await permission.save()
return { return {
permissionId: savedPermission.permissionId, uid: savedPermission.uid,
path: savedPermission.path, path: savedPermission.path,
type: savedPermission.type, type: savedPermission.type,
setting: savedPermission.setting, setting: savedPermission.setting,
@@ -324,27 +324,21 @@ const createPermission = async ({
} }
const updatePermission = async ( const updatePermission = async (
id: number, uid: string,
data: UpdatePermissionPayload data: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> => { ): Promise<PermissionDetailsResponse> => {
const { setting } = data const { setting } = data
const updatedPermission = (await Permission.findOneAndUpdate( const updatedPermission = (await Permission.findOneAndUpdate(
{ permissionId: id }, { _id: uid },
{ setting }, { setting },
{ new: true } { new: true }
) )
.select({ .select('uid path type setting')
_id: 0, .populate({ path: 'user', select: 'uid username displayName isAdmin' })
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({ .populate({
path: 'group', path: 'group',
select: 'groupId name description -_id' select: 'groupId name description'
})) as unknown as PermissionDetailsResponse })) as unknown as PermissionDetailsResponse
if (!updatedPermission) if (!updatedPermission)
throw { throw {
@@ -356,13 +350,13 @@ const updatePermission = async (
return updatedPermission return updatedPermission
} }
const deletePermission = async (id: number) => { const deletePermission = async (uid: string) => {
const permission = await Permission.findOne({ permissionId: id }) const permission = await Permission.findOne({ _id: uid })
if (!permission) if (!permission)
throw { throw {
code: 404, code: 404,
status: 'Not Found', status: 'Not Found',
message: 'Permission not found.' message: 'Permission not found.'
} }
await Permission.deleteOne({ permissionId: id }) await Permission.deleteOne({ _id: uid })
} }

View File

@@ -1,11 +1,10 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa' import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user' import { UserResponse } from './user'
import { getSessionController } from './internal'
import { SessionState } from '../types'
interface SessionResponse extends UserResponse { interface SessionResponse extends Omit<UserResponse, 'uid'> {
needsToUpdatePassword: boolean id: string
needsToUpdatePassword?: boolean
} }
@Security('bearerAuth') @Security('bearerAuth')
@@ -16,11 +15,12 @@ export class SessionController {
* @summary Get session info (username). * @summary Get session info (username).
* *
*/ */
@Example<UserResponse>({ @Example<SessionResponse>({
id: 123, id: 'userIdString',
username: 'johnusername', username: 'johnusername',
displayName: 'John', displayName: 'John',
isAdmin: false isAdmin: false,
needsToUpdatePassword: false
}) })
@Get('/') @Get('/')
public async session( public async session(
@@ -28,18 +28,6 @@ export class SessionController {
): Promise<SessionResponse> { ): Promise<SessionResponse> {
return session(request) return session(request)
} }
/**
* The polling endpoint is currently implemented for single-server deployments only.<br>
* Load balanced / grid topologies will be supported in a future release.<br>
* If your site requires this, please reach out to SASjs Support.
* @summary Get session state (initialising, pending, running, completed, failed).
* @example completed
*/
@Get('/:sessionId/state')
public async sessionState(sessionId: string): Promise<SessionState> {
return sessionState(sessionId)
}
} }
const session = (req: express.Request) => ({ const session = (req: express.Request) => ({
@@ -49,23 +37,3 @@ const session = (req: express.Request) => ({
isAdmin: req.user!.isAdmin, isAdmin: req.user!.isAdmin,
needsToUpdatePassword: req.user!.needsToUpdatePassword needsToUpdatePassword: req.user!.needsToUpdatePassword
}) })
const sessionState = (sessionId: string): SessionState => {
for (let runTime of process.runTimes) {
// get session controller for each available runTime
const sessionController = getSessionController(runTime)
// get session by sessionId
const session = sessionController.getSessionById(sessionId)
// return session state if session was found
if (session) {
return session.state
}
}
throw {
code: 404,
message: `Session with ID '${sessionId}' was not found.`
}
}

View File

@@ -1,12 +1,10 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa' import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { import { ExecutionController, ExecutionVars } from './internal'
ExecutionController,
ExecutionVars,
getSessionController
} from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
HTTPHeaders,
LogLine,
makeFilesNamesMap, makeFilesNamesMap,
getRunTimeAndFilePath getRunTimeAndFilePath
} from '../utils' } from '../utils'
@@ -20,36 +18,6 @@ interface ExecutePostRequestPayload {
_program?: string _program?: string
} }
interface TriggerProgramPayload {
/**
* Location of SAS program.
* @example "/Public/somefolder/some.file"
*/
_program: string
/**
* Amount of minutes after the completion of the program when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
/**
* Query param for setting debug mode.
*/
_debug?: number
}
interface TriggerProgramResponse {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store program outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/stp') @Route('SASjsApi/stp')
@Tags('STP') @Tags('STP')
@@ -57,31 +25,20 @@ export class STPController {
/** /**
* Trigger a Stored Program using the _program URL parameter. * Trigger a Stored Program using the _program URL parameter.
* *
* Accepts additional URL parameters (converted to session variables) * Accepts URL parameters and file uploads. For more details, see docs:
* and file uploads. For more details, see docs:
* *
* https://server.sasjs.io/storedprograms * https://server.sasjs.io/storedprograms
* *
* @summary Execute a Stored Program, returns _webout and (optionally) log. * @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of Stored Program in SASjs Drive. * @param _program Location of code in SASjs Drive
* @param _debug Optional query param for setting debug mode (returns the session log in the response body).
* @example _program "/Projects/myApp/some/program" * @example _program "/Projects/myApp/some/program"
* @example _debug 131
*/ */
@Get('/execute') @Get('/execute')
public async executeGetRequest( public async executeGetRequest(
@Request() request: express.Request, @Request() request: express.Request,
@Query() _program: string, @Query() _program: string
@Query() _debug?: number
): Promise<string | Buffer> { ): Promise<string | Buffer> {
let vars = request.query as ExecutionVars const vars = request.query as ExecutionVars
if (_debug) {
vars = {
...vars,
_debug
}
}
return execute(request, _program, vars) return execute(request, _program, vars)
} }
@@ -112,26 +69,6 @@ export class STPController {
return execute(request, program!, vars, otherArgs) return execute(request, program!, vars, otherArgs)
} }
/**
* Trigger Program on the Specified Runtime.
* @summary Triggers program and returns SessionId immediately - does not wait for program completion.
* @param _program Location of code in SASjs Drive.
* @param expiresAfterMins Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.
* @param _debug Optional query param for setting debug mode.
* @example _program "/Projects/myApp/some/program"
* @example _debug 131
* @example expiresAfterMins 15
*/
@Post('/trigger')
public async triggerProgram(
@Request() request: express.Request,
@Query() _program: string,
@Query() _debug?: number,
@Query() expiresAfterMins?: number
): Promise<TriggerProgramResponse> {
return triggerProgram(request, { _program, _debug, expiresAfterMins })
}
} }
const execute = async ( const execute = async (
@@ -170,52 +107,3 @@ const execute = async (
} }
} }
} }
const triggerProgram = async (
req: express.Request,
{ _program, _debug, expiresAfterMins }: TriggerProgramPayload
): Promise<TriggerProgramResponse> => {
try {
// put _program query param into vars object
const vars: { [key: string]: string | number } = { _program }
// if present add _debug query param to vars object
if (_debug) {
vars._debug = _debug
}
// get code path and runTime
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
// call executeFile method of ExecutionController without awaiting
new ExecutionController().executeFile({
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars,
session
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -26,18 +26,14 @@ import {
import { GroupController, GroupResponse } from './group' import { GroupController, GroupResponse } from './group'
export interface UserResponse { export interface UserResponse {
id: number uid: string
username: string username: string
displayName: string displayName: string
isAdmin: boolean isAdmin: boolean
} }
export interface UserDetailsResponse { export interface UserDetailsResponse extends UserResponse {
id: number
displayName: string
username: string
isActive: boolean isActive: boolean
isAdmin: boolean
autoExec?: string autoExec?: string
groups?: GroupResponse[] groups?: GroupResponse[]
} }
@@ -52,13 +48,13 @@ export class UserController {
*/ */
@Example<UserResponse[]>([ @Example<UserResponse[]>([
{ {
id: 123, uid: 'userIdString',
username: 'johnusername', username: 'johnusername',
displayName: 'John', displayName: 'John',
isAdmin: false isAdmin: false
}, },
{ {
id: 456, uid: 'anotherUserIdString',
username: 'starkusername', username: 'starkusername',
displayName: 'Stark', displayName: 'Stark',
isAdmin: true isAdmin: true
@@ -74,7 +70,7 @@ export class UserController {
* *
*/ */
@Example<UserDetailsResponse>({ @Example<UserDetailsResponse>({
id: 1234, uid: 'userIdString',
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
isAdmin: false, isAdmin: false,
@@ -111,20 +107,20 @@ export class UserController {
* Only Admin or user itself will get user autoExec code. * Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName. * @summary Get user properties - such as group memberships, userName, displayName.
* @param userId The user's identifier * @param userId The user's identifier
* @example userId 1234 * @example userId "userIdString"
*/ */
@Get('{userId}') @Get('{uid}')
public async getUser( public async getUser(
@Request() req: express.Request, @Request() req: express.Request,
@Path() userId: number @Path() uid: string
): Promise<UserDetailsResponse> { ): Promise<UserDetailsResponse> {
const { MODE } = process.env const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec() if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req const { user } = req
const getAutoExec = user!.isAdmin || user!.userId == userId const getAutoExec = user!.isAdmin || user!.userId === uid
return getUser({ id: userId }, getAutoExec) return getUser({ _id: uid }, getAutoExec)
} }
/** /**
@@ -133,7 +129,7 @@ export class UserController {
* @example username "johnSnow01" * @example username "johnSnow01"
*/ */
@Example<UserDetailsResponse>({ @Example<UserDetailsResponse>({
id: 1234, uid: 'userIdString',
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
isAdmin: false, isAdmin: false,
@@ -158,7 +154,7 @@ export class UserController {
* @example userId "1234" * @example userId "1234"
*/ */
@Example<UserDetailsResponse>({ @Example<UserDetailsResponse>({
id: 1234, uid: 'userIdString',
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
isAdmin: false, isAdmin: false,
@@ -166,7 +162,7 @@ export class UserController {
}) })
@Patch('{userId}') @Patch('{userId}')
public async updateUser( public async updateUser(
@Path() userId: number, @Path() userId: string,
@Body() body: UserPayload @Body() body: UserPayload
): Promise<UserDetailsResponse> { ): Promise<UserDetailsResponse> {
const { MODE } = process.env const { MODE } = process.env
@@ -174,7 +170,7 @@ export class UserController {
if (MODE === ModeType.Desktop) if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '') return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser({ id: userId }, body) return updateUser({ _id: userId }, body)
} }
/** /**
@@ -198,18 +194,16 @@ export class UserController {
*/ */
@Delete('{userId}') @Delete('{userId}')
public async deleteUser( public async deleteUser(
@Path() userId: number, @Path() userId: string,
@Body() body: { password?: string }, @Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false @Query() @Hidden() isAdmin: boolean = false
) { ) {
return deleteUser({ id: userId }, isAdmin, body) return deleteUser({ _id: userId }, isAdmin, body)
} }
} }
const getAllUsers = async (): Promise<UserResponse[]> => const getAllUsers = async (): Promise<UserResponse[]> =>
await User.find({}) await User.find({}).select('uid username displayName isAdmin').exec()
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
.exec()
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => { const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive, autoExec } = data const { displayName, username, password, isAdmin, isActive, autoExec } = data
@@ -239,15 +233,15 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const groupController = new GroupController() const groupController = new GroupController()
const allUsersGroup = await groupController const allUsersGroup = await groupController
.getGroupByGroupName(ALL_USERS_GROUP.name) .getGroupByName(ALL_USERS_GROUP.name)
.catch(() => {}) .catch(() => {})
if (allUsersGroup) { if (allUsersGroup) {
await groupController.addUserToGroup(allUsersGroup.groupId, savedUser.id) await groupController.addUserToGroup(allUsersGroup.uid, savedUser.uid)
} }
return { return {
id: savedUser.id, uid: savedUser.uid,
displayName: savedUser.displayName, displayName: savedUser.displayName,
username: savedUser.username, username: savedUser.username,
isActive: savedUser.isActive, isActive: savedUser.isActive,
@@ -256,8 +250,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
} }
} }
interface GetUserBy { export interface GetUserBy {
id?: number _id?: string
username?: string username?: string
} }
@@ -267,10 +261,10 @@ const getUser = async (
): Promise<UserDetailsResponse> => { ): Promise<UserDetailsResponse> => {
const user = (await User.findOne( const user = (await User.findOne(
findBy, findBy,
`id displayName username isActive isAdmin autoExec -_id` `uid displayName username isActive isAdmin autoExec`
).populate( ).populate(
'groups', 'groups',
'groupId name description -_id' 'uid name description'
)) as unknown as UserDetailsResponse )) as unknown as UserDetailsResponse
if (!user) if (!user)
@@ -280,12 +274,12 @@ const getUser = async (
} }
return { return {
id: user.id, uid: user.uid,
displayName: user.displayName, displayName: user.displayName,
username: user.username, username: user.username,
isActive: user.isActive, isActive: user.isActive,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
autoExec: getAutoExec ? (user.autoExec ?? '') : undefined, autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
groups: user.groups groups: user.groups
} }
} }
@@ -293,7 +287,7 @@ const getUser = async (
const getDesktopAutoExec = async () => { const getDesktopAutoExec = async () => {
return { return {
...desktopUser, ...desktopUser,
id: desktopUser.userId, uid: desktopUser.userId,
autoExec: await getUserAutoExec() autoExec: await getUserAutoExec()
} }
} }
@@ -329,8 +323,8 @@ const updateUser = async (
const usernameExist = await User.findOne({ username }) const usernameExist = await User.findOne({ username })
if (usernameExist) { if (usernameExist) {
if ( if (
(findBy.id && usernameExist.id != findBy.id) || (findBy._id && usernameExist.uid !== findBy._id) ||
(findBy.username && usernameExist.username != findBy.username) (findBy.username && usernameExist.username !== findBy.username)
) )
throw { throw {
code: 409, code: 409,
@@ -350,11 +344,11 @@ const updateUser = async (
if (!updatedUser) if (!updatedUser)
throw { throw {
code: 404, code: 404,
message: `Unable to find user with ${findBy.id || findBy.username}` message: `Unable to find user with ${findBy._id || findBy.username}`
} }
return { return {
id: updatedUser.id, uid: updatedUser.uid,
username: updatedUser.username, username: updatedUser.username,
displayName: updatedUser.displayName, displayName: updatedUser.displayName,
isAdmin: updatedUser.isAdmin, isAdmin: updatedUser.isAdmin,
@@ -367,7 +361,7 @@ const updateDesktopAutoExec = async (autoExec: string) => {
await updateUserAutoExec(autoExec) await updateUserAutoExec(autoExec)
return { return {
...desktopUser, ...desktopUser,
id: desktopUser.userId, uid: desktopUser.userId,
autoExec autoExec
} }
} }

View File

@@ -76,7 +76,7 @@ const authenticateToken = async (
const { MODE } = process.env const { MODE } = process.env
if (MODE === ModeType.Desktop) { if (MODE === ModeType.Desktop) {
req.user = { req.user = {
userId: 1234, userId: '1234',
clientId: 'desktopModeClientId', clientId: 'desktopModeClientId',
username: 'desktopModeUsername', username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName', displayName: 'desktopModeDisplayName',

View File

@@ -18,7 +18,7 @@ export const authorize: RequestHandler = async (req, res, next) => {
// no need to check for permissions when route is Public // no need to check for permissions when route is Public
if (await isPublicRoute(req)) return next() if (await isPublicRoute(req)) return next()
const dbUser = await User.findOne({ id: user.userId }) const dbUser = await User.findOne({ _id: user.userId })
if (!dbUser) return res.sendStatus(401) if (!dbUser) return res.sendStatus(401)
const path = getPath(req) const path = getPath(req)

View File

@@ -28,7 +28,7 @@ export const desktopRestrict: RequestHandler = (req, res, next) => {
} }
export const desktopUser: RequestUser = { export const desktopUser: RequestUser = {
userId: 12345, userId: '12345',
clientId: 'desktop_app', clientId: 'desktop_app',
username: userInfo().username, username: userInfo().username,
displayName: userInfo().username, displayName: userInfo().username,

View File

@@ -8,8 +8,8 @@ export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
if (!user?.isAdmin) { if (!user?.isAdmin) {
let adminAccountRequired: boolean = true let adminAccountRequired: boolean = true
if (req.params.userId) { if (req.params.uid) {
adminAccountRequired = user?.userId !== parseInt(req.params.userId) adminAccountRequired = user?.userId !== req.params.uid
} else if (req.params.username) { } else if (req.params.username) {
adminAccountRequired = user?.username !== req.params.username adminAccountRequired = user?.username !== req.params.username
} }

View File

@@ -1,15 +0,0 @@
import mongoose, { Schema } from 'mongoose'
const CounterSchema = new Schema({
id: {
type: String,
required: true,
unique: true
},
seq: {
type: Number,
required: true
}
})
export default mongoose.model('Counter', CounterSchema)

View File

@@ -1,9 +1,9 @@
import { Schema, model, Document, Model } from 'mongoose' import { Schema, model, Document, Model } from 'mongoose'
import { GroupDetailsResponse } from '../controllers' import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User' import User, { IUser } from './User'
import { AuthProviderType, getSequenceNextValue } from '../utils' import { AuthProviderType } from '../utils'
export const PUBLIC_GROUP_NAME = 'Public' export const PUBLIC_GROUP_NAME = 'public'
export interface GroupPayload { export interface GroupPayload {
/** /**
@@ -24,10 +24,12 @@ export interface GroupPayload {
} }
interface IGroupDocument extends GroupPayload, Document { interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean isActive: boolean
users: Schema.Types.ObjectId[] users: Schema.Types.ObjectId[]
authProvider?: AuthProviderType authProvider?: AuthProviderType
// Declare virtual properties as read-only properties
readonly uid: string
} }
interface IGroup extends IGroupDocument { interface IGroup extends IGroupDocument {
@@ -37,46 +39,52 @@ interface IGroup extends IGroupDocument {
} }
interface IGroupModel extends Model<IGroup> {} interface IGroupModel extends Model<IGroup> {}
const groupSchema = new Schema<IGroupDocument>({ const opts = {
name: { toJSON: {
type: String, virtuals: true,
required: true, transform: function (doc: any, ret: any, options: any) {
unique: true delete ret._id
}, delete ret.id
groupId: { return ret
type: Number, }
unique: true
},
description: {
type: String,
default: 'Group description.'
},
authProvider: {
type: String,
enum: AuthProviderType
},
isActive: {
type: Boolean,
default: true
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
// Hooks
groupSchema.pre('save', async function () {
if (this.isNew) {
this.groupId = await getSequenceNextValue('groupId')
} }
}
const groupSchema = new Schema<IGroupDocument>(
{
name: {
type: String,
required: true,
unique: true
},
description: {
type: String,
default: 'Group description.'
},
authProvider: {
type: String,
enum: AuthProviderType
},
isActive: {
type: Boolean,
default: true
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
},
opts
)
groupSchema.virtual('uid').get(function () {
return this._id.toString()
}) })
groupSchema.post('save', function (group: IGroup, next: Function) { groupSchema.post('save', function (group: IGroup, next: Function) {
group.populate('users', 'id username displayName -_id').then(function () { group.populate('users', 'uid username displayName').then(function () {
next() next()
}) })
}) })
// pre remove hook to remove all references of group from users // pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function (this: IGroupDocument) { groupSchema.pre('remove', async function () {
const userIds = this.users const userIds = this.users
await Promise.all( await Promise.all(
userIds.map(async (userId) => { userIds.map(async (userId) => {

View File

@@ -1,6 +1,5 @@
import { Schema, model, Document, Model } from 'mongoose' import { Schema, model, Document, Model } from 'mongoose'
import { PermissionDetailsResponse } from '../controllers' import { PermissionDetailsResponse } from '../controllers'
import { getSequenceNextValue } from '../utils'
interface GetPermissionBy { interface GetPermissionBy {
user?: Schema.Types.ObjectId user?: Schema.Types.ObjectId
@@ -11,9 +10,11 @@ interface IPermissionDocument extends Document {
path: string path: string
type: string type: string
setting: string setting: string
permissionId: number
user: Schema.Types.ObjectId user: Schema.Types.ObjectId
group: Schema.Types.ObjectId group: Schema.Types.ObjectId
// Declare virtual properties as read-only properties
readonly uid: string
} }
interface IPermission extends IPermissionDocument {} interface IPermission extends IPermissionDocument {}
@@ -22,32 +23,39 @@ interface IPermissionModel extends Model<IPermission> {
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]> get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
} }
const permissionSchema = new Schema<IPermissionDocument>({ const opts = {
permissionId: { toJSON: {
type: Number, virtuals: true,
unique: true transform: function (doc: any, ret: any, options: any) {
}, delete ret._id
path: { delete ret.id
type: String, return ret
required: true }
},
type: {
type: String,
required: true
},
setting: {
type: String,
required: true
},
user: { type: Schema.Types.ObjectId, ref: 'User' },
group: { type: Schema.Types.ObjectId, ref: 'Group' }
})
// Hooks
permissionSchema.pre('save', async function () {
if (this.isNew) {
this.permissionId = await getSequenceNextValue('permissionId')
} }
}
const permissionSchema = new Schema<IPermissionDocument>(
{
path: {
type: String,
required: true
},
type: {
type: String,
required: true
},
setting: {
type: String,
required: true
},
user: { type: Schema.Types.ObjectId, ref: 'User' },
group: { type: Schema.Types.ObjectId, ref: 'Group' }
},
opts
)
permissionSchema.virtual('uid').get(function () {
return this._id.toString()
}) })
// Static Methods // Static Methods
@@ -55,20 +63,14 @@ permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
PermissionDetailsResponse[] PermissionDetailsResponse[]
> { > {
return (await this.find(getBy) return (await this.find(getBy)
.select({ .select('uid path type setting')
_id: 0, .populate({ path: 'user', select: 'uid username displayName isAdmin' })
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({ .populate({
path: 'group', path: 'group',
select: 'groupId name description -_id', select: 'uid name description',
populate: { populate: {
path: 'users', path: 'users',
select: 'id username displayName isAdmin -_id', select: 'uid username displayName isAdmin',
options: { limit: 15 } options: { limit: 15 }
} }
})) as unknown as PermissionDetailsResponse[] })) as unknown as PermissionDetailsResponse[]

View File

@@ -1,6 +1,6 @@
import { Schema, model, Document, Model } from 'mongoose' import { Schema, model, Document, Model, ObjectId } from 'mongoose'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import { AuthProviderType, getSequenceNextValue } from '../utils' import { AuthProviderType } from '../utils'
export interface UserPayload { export interface UserPayload {
/** /**
@@ -36,7 +36,6 @@ export interface UserPayload {
interface IUserDocument extends UserPayload, Document { interface IUserDocument extends UserPayload, Document {
_id: Schema.Types.ObjectId _id: Schema.Types.ObjectId
id: number
isAdmin: boolean isAdmin: boolean
isActive: boolean isActive: boolean
needsToUpdatePassword: boolean needsToUpdatePassword: boolean
@@ -44,6 +43,9 @@ interface IUserDocument extends UserPayload, Document {
groups: Schema.Types.ObjectId[] groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }] tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType authProvider?: AuthProviderType
// Declare virtual properties as read-only properties
readonly uid: string
} }
export interface IUser extends IUserDocument { export interface IUser extends IUserDocument {
@@ -54,70 +56,74 @@ export interface IUser extends IUserDocument {
interface IUserModel extends Model<IUser> { interface IUserModel extends Model<IUser> {
hashPassword(password: string): string hashPassword(password: string): string
} }
const opts = {
const userSchema = new Schema<IUserDocument>({ toJSON: {
displayName: { virtuals: true,
type: String, transform: function (doc: any, ret: any, options: any) {
required: true delete ret._id
}, delete ret.id
username: { return ret
type: String,
required: true,
unique: true
},
id: {
type: Number,
unique: true
},
password: {
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType
},
isAdmin: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
needsToUpdatePassword: {
type: Boolean,
default: true
},
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [
{
clientId: {
type: String,
required: true
},
accessToken: {
type: String,
required: true
},
refreshToken: {
type: String,
required: true
}
} }
]
})
// Hooks
userSchema.pre('save', async function (next) {
if (this.isNew) {
this.id = await getSequenceNextValue('id')
} }
}
next() const userSchema = new Schema<IUserDocument>(
{
displayName: {
type: String,
required: true
},
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType
},
isAdmin: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
needsToUpdatePassword: {
type: Boolean,
default: true
},
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [
{
clientId: {
type: String,
required: true
},
accessToken: {
type: String,
required: true
},
refreshToken: {
type: String,
required: true
}
}
]
},
opts
)
userSchema.virtual('uid').get(function () {
return this._id.toString()
}) })
// Static Methods // Static Methods

View File

@@ -1,5 +1,5 @@
import express from 'express' import express from 'express'
import { runCodeValidation, triggerCodeValidation } from '../../utils' import { runCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/' import { CodeController } from '../../controllers/'
const runRouter = express.Router() const runRouter = express.Router()
@@ -28,22 +28,4 @@ runRouter.post('/execute', async (req, res) => {
} }
}) })
runRouter.post('/trigger', async (req, res) => {
const { error, value: body } = triggerCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerCode(req, body)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter export default runRouter

View File

@@ -1,7 +1,11 @@
import express from 'express' import express from 'express'
import { GroupController } from '../../controllers/' import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares' import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import { getGroupValidation, registerGroupValidation } from '../../utils' import {
getGroupValidation,
registerGroupValidation,
uidValidation
} from '../../utils'
const groupRouter = express.Router() const groupRouter = express.Router()
@@ -33,12 +37,15 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
} }
}) })
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => { groupRouter.get('/:uid', authenticateAccessToken, async (req, res) => {
const { groupId } = req.params const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.getGroup(parseInt(groupId)) const response = await controller.getGroup(uid)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -56,7 +63,7 @@ groupRouter.get(
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.getGroupByGroupName(name) const response = await controller.getGroupByName(name)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -65,18 +72,15 @@ groupRouter.get(
) )
groupRouter.post( groupRouter.post(
'/:groupId/:userId', '/:groupUid/:userUid',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req, res) => { async (req, res) => {
const { groupId, userId } = req.params const { groupUid, userUid } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.addUserToGroup( const response = await controller.addUserToGroup(groupUid, userUid)
parseInt(groupId),
parseInt(userId)
)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -85,18 +89,15 @@ groupRouter.post(
) )
groupRouter.delete( groupRouter.delete(
'/:groupId/:userId', '/:groupUid/:userUid',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req, res) => { async (req, res) => {
const { groupId, userId } = req.params const { groupUid, userUid } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.removeUserFromGroup( const response = await controller.removeUserFromGroup(groupUid, userUid)
parseInt(groupId),
parseInt(userId)
)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -105,15 +106,18 @@ groupRouter.delete(
) )
groupRouter.delete( groupRouter.delete(
'/:groupId', '/:uid',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req, res) => { async (req, res) => {
const { groupId } = req.params const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const controller = new GroupController() const controller = new GroupController()
try { try {
await controller.deleteGroup(parseInt(groupId)) await controller.deleteGroup(uid)
res.status(200).send('Group Deleted!') res.status(200).send('Group Deleted!')
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)

View File

@@ -3,6 +3,7 @@ import { PermissionController } from '../../controllers/'
import { verifyAdmin } from '../../middlewares' import { verifyAdmin } from '../../middlewares'
import { import {
registerPermissionValidation, registerPermissionValidation,
uidValidation,
updatePermissionValidation updatePermissionValidation
} from '../../utils' } from '../../utils'
@@ -34,14 +35,17 @@ permissionRouter.post('/', verifyAdmin, async (req, res) => {
} }
}) })
permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => { permissionRouter.patch('/:uid', verifyAdmin, async (req: any, res) => {
const { permissionId } = req.params const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const { error, value: body } = updatePermissionValidation(req.body) const { error, value: body } = updatePermissionValidation(req.body)
if (error) return res.status(400).send(error.details[0].message) if (error) return res.status(400).send(error.details[0].message)
try { try {
const response = await controller.updatePermission(permissionId, body) const response = await controller.updatePermission(uid, body)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
const statusCode = err.code const statusCode = err.code
@@ -50,20 +54,18 @@ permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
} }
}) })
permissionRouter.delete( permissionRouter.delete('/:uid', verifyAdmin, async (req: any, res) => {
'/:permissionId', const { error: uidError, value: params } = uidValidation(req.params)
verifyAdmin, if (uidError) return res.status(400).send(uidError.details[0].message)
async (req: any, res) => {
const { permissionId } = req.params
try { const { uid } = params
await controller.deletePermission(permissionId) try {
res.status(200).send('Permission Deleted!') await controller.deletePermission(uid)
} catch (err: any) { res.status(200).send('Permission Deleted!')
const statusCode = err.code } catch (err: any) {
delete err.code const statusCode = err.code
res.status(statusCode).send(err.message) delete err.code
} res.status(statusCode).send(err.message)
} }
) })
export default permissionRouter export default permissionRouter

View File

@@ -1,37 +1,16 @@
import express from 'express' import express from 'express'
import { SessionController } from '../../controllers' import { SessionController } from '../../controllers'
import { sessionIdValidation } from '../../utils'
const sessionRouter = express.Router() const sessionRouter = express.Router()
const controller = new SessionController()
sessionRouter.get('/', async (req, res) => { sessionRouter.get('/', async (req, res) => {
const controller = new SessionController()
try { try {
const response = await controller.session(req) const response = await controller.session(req)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
} }
}) })
sessionRouter.get('/:sessionId/state', async (req, res) => {
const { error, value: params } = sessionIdValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.sessionState(params.sessionId)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default sessionRouter export default sessionRouter

View File

@@ -13,6 +13,7 @@ import {
generateAccessToken, generateAccessToken,
generateAuthCode, generateAuthCode,
generateRefreshToken, generateRefreshToken,
randomBytesHexString,
saveTokensInDB, saveTokensInDB,
verifyTokenInDB verifyTokenInDB
} from '../../../utils' } from '../../../utils'
@@ -20,7 +21,6 @@ import {
const clientId = 'someclientID' const clientId = 'someclientID'
const clientSecret = 'someclientSecret' const clientSecret = 'someclientSecret'
const user = { const user = {
id: 1234,
displayName: 'Test User', displayName: 'Test User',
username: 'testUsername', username: 'testUsername',
password: '87654321', password: '87654321',
@@ -52,7 +52,7 @@ describe('auth', () => {
describe('token', () => { describe('token', () => {
const userInfo: InfoJWT = { const userInfo: InfoJWT = {
clientId, clientId,
userId: user.id userId: randomBytesHexString(12)
} }
beforeAll(async () => { beforeAll(async () => {
await userController.createUser(user) await userController.createUser(user)
@@ -151,10 +151,10 @@ describe('auth', () => {
currentUser = await userController.createUser(user) currentUser = await userController.createUser(user)
refreshToken = generateRefreshToken({ refreshToken = generateRefreshToken({
clientId, clientId,
userId: currentUser.id userId: currentUser.uid
}) })
await saveTokensInDB( await saveTokensInDB(
currentUser.id, currentUser.uid,
clientId, clientId,
'accessToken', 'accessToken',
refreshToken refreshToken
@@ -202,11 +202,11 @@ describe('auth', () => {
currentUser = await userController.createUser(user) currentUser = await userController.createUser(user)
accessToken = generateAccessToken({ accessToken = generateAccessToken({
clientId, clientId,
userId: currentUser.id userId: currentUser.uid
}) })
await saveTokensInDB( await saveTokensInDB(
currentUser.id, currentUser.uid,
clientId, clientId,
accessToken, accessToken,
'refreshToken' 'refreshToken'

View File

@@ -40,10 +40,10 @@ describe('client', () => {
const dbUser = await userController.createUser(adminUser) const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({ adminAccessToken = generateAccessToken({
clientId: client.clientId, clientId: client.clientId,
userId: dbUser.id userId: dbUser.uid
}) })
await saveTokensInDB( await saveTokensInDB(
dbUser.id, dbUser.uid,
client.clientId, client.clientId,
adminAccessToken, adminAccessToken,
'refreshToken' 'refreshToken'
@@ -95,10 +95,10 @@ describe('client', () => {
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId: client.clientId, clientId: client.clientId,
userId: dbUser.id userId: dbUser.uid
}) })
await saveTokensInDB( await saveTokensInDB(
dbUser.id, dbUser.uid,
client.clientId, client.clientId,
accessToken, accessToken,
'refreshToken' 'refreshToken'
@@ -212,10 +212,10 @@ describe('client', () => {
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId: client.clientId, clientId: client.clientId,
userId: dbUser.id userId: dbUser.uid
}) })
await saveTokensInDB( await saveTokensInDB(
dbUser.id, dbUser.uid,
client.clientId, client.clientId,
accessToken, accessToken,
'refreshToken' 'refreshToken'

View File

@@ -71,31 +71,31 @@ describe('drive', () => {
con = await mongoose.connect(mongoServer.getUri()) con = await mongoose.connect(mongoServer.getUri())
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
accessToken = await generateAndSaveToken(dbUser.id) accessToken = await generateAndSaveToken(dbUser.uid)
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/deploy', path: '/SASjsApi/drive/deploy',
principalId: dbUser.id principalId: dbUser.uid
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/deploy/upload', path: '/SASjsApi/drive/deploy/upload',
principalId: dbUser.id principalId: dbUser.uid
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/file', path: '/SASjsApi/drive/file',
principalId: dbUser.id principalId: dbUser.uid
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/folder', path: '/SASjsApi/drive/folder',
principalId: dbUser.id principalId: dbUser.uid
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/rename', path: '/SASjsApi/drive/rename',
principalId: dbUser.id principalId: dbUser.uid
}) })
}) })
@@ -1197,7 +1197,7 @@ const getExampleService = (): ServiceMember =>
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember) ((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
.members[0] as ServiceMember .members[0] as ServiceMember
const generateAndSaveToken = async (userId: number) => { const generateAndSaveToken = async (userId: string) => {
const adminAccessToken = generateAccessToken({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId

View File

@@ -11,6 +11,7 @@ import {
} from '../../../utils' } from '../../../utils'
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group' import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
import User from '../../../model/User' import User from '../../../model/User'
import { randomBytes } from 'crypto'
const clientId = 'someclientID' const clientId = 'someclientID'
const adminUser = { const adminUser = {
@@ -75,7 +76,7 @@ describe('group', () => {
.send(group) .send(group)
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -155,7 +156,7 @@ describe('group', () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`) .delete(`/SASjsApi/group/${dbGroup.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -174,17 +175,17 @@ describe('group', () => {
username: 'deletegroup2' username: 'deletegroup2'
}) })
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id) await groupController.addUserToGroup(dbGroup.uid, dbUser1.uid)
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id) await groupController.addUserToGroup(dbGroup.uid, dbUser2.uid)
await request(app) await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`) .delete(`/SASjsApi/group/${dbGroup.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
const res1 = await request(app) const res1 = await request(app)
.get(`/SASjsApi/user/${dbUser1.id}`) .get(`/SASjsApi/user/${dbUser1.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -192,7 +193,7 @@ describe('group', () => {
expect(res1.body.groups).toEqual([]) expect(res1.body.groups).toEqual([])
const res2 = await request(app) const res2 = await request(app)
.get(`/SASjsApi/user/${dbUser2.id}`) .get(`/SASjsApi/user/${dbUser2.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -201,8 +202,10 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/1234`) .delete(`/SASjsApi/group/${hexValue}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -229,7 +232,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`) .delete(`/SASjsApi/group/${dbGroup.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(401) .expect(401)
@@ -245,15 +248,15 @@ describe('group', () => {
}) })
it('should respond with group', async () => { it('should respond with group', async () => {
const { groupId } = await groupController.createGroup(group) const { uid } = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/group/${groupId}`) .get(`/SASjsApi/group/${uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -266,15 +269,15 @@ describe('group', () => {
username: 'get' + user.username username: 'get' + user.username
}) })
const { groupId } = await groupController.createGroup(group) const { uid } = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/group/${groupId}`) .get(`/SASjsApi/group/${uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -292,8 +295,10 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.get('/SASjsApi/group/1234') .get(`/SASjsApi/group/${hexValue}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -312,7 +317,7 @@ describe('group', () => {
.send() .send()
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -333,7 +338,7 @@ describe('group', () => {
.send() .send()
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -379,7 +384,7 @@ describe('group', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
groupId: expect.anything(), uid: expect.anything(),
name: group.name, name: group.name,
description: group.description description: group.description
} }
@@ -401,7 +406,7 @@ describe('group', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
groupId: expect.anything(), uid: expect.anything(),
name: group.name, name: group.name,
description: group.description description: group.description
} }
@@ -426,18 +431,18 @@ describe('group', () => {
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([ expect(res.body.users).toEqual([
{ {
id: expect.anything(), uid: expect.anything(),
username: user.username, username: user.username,
displayName: user.displayName displayName: user.displayName
} }
@@ -452,20 +457,20 @@ describe('group', () => {
}) })
await request(app) await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`) .get(`/SASjsApi/user/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.groups).toEqual([ expect(res.body.groups).toEqual([
{ {
groupId: expect.anything(), uid: expect.anything(),
name: group.name, name: group.name,
description: group.description description: group.description
} }
@@ -478,21 +483,21 @@ describe('group', () => {
...user, ...user,
username: 'addUserRandomUser' username: 'addUserRandomUser'
}) })
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id) await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([ expect(res.body.users).toEqual([
{ {
id: expect.anything(), uid: expect.anything(),
username: 'addUserRandomUser', username: 'addUserRandomUser',
displayName: user.displayName displayName: user.displayName
} }
@@ -526,8 +531,10 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.post('/SASjsApi/group/123/123') .post(`/SASjsApi/group/${hexValue}/123`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -538,8 +545,10 @@ describe('group', () => {
it('should respond with Not Found if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/123`) .post(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -556,7 +565,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(400) .expect(400)
@@ -577,7 +586,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -596,7 +605,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -618,15 +627,15 @@ describe('group', () => {
...user, ...user,
username: 'removeUserRandomUser' username: 'removeUserRandomUser'
}) })
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id) await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.groupId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -639,16 +648,16 @@ describe('group', () => {
...user, ...user,
username: 'removeGroupFromUser' username: 'removeGroupFromUser'
}) })
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id) await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
await request(app) await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`) .get(`/SASjsApi/user/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -667,7 +676,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -686,7 +695,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`) .delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -723,8 +732,10 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.delete('/SASjsApi/group/123/123') .delete(`/SASjsApi/group/${hexValue}/123`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -735,8 +746,10 @@ describe('group', () => {
it('should respond with Not Found if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`) .delete(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -752,10 +765,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => { ): Promise<string> => {
const dbUser = await userController.createUser(someUser ?? adminUser) const dbUser = await userController.createUser(someUser ?? adminUser)
return generateAndSaveToken(dbUser.id) return generateAndSaveToken(dbUser.uid)
} }
const generateAndSaveToken = async (userId: number) => { const generateAndSaveToken = async (userId: string) => {
const adminAccessToken = generateAccessToken({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId

View File

@@ -17,6 +17,7 @@ import {
PermissionDetailsResponse PermissionDetailsResponse
} from '../../../controllers' } from '../../../controllers'
import { generateAccessToken, saveTokensInDB } from '../../../utils' import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { randomBytes } from 'crypto'
const deployPayload = { const deployPayload = {
appLoc: 'string', appLoc: 'string',
@@ -103,10 +104,10 @@ describe('permission', () => {
const res = await request(app) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.id }) .send({ ...permission, principalId: dbUser.uid })
.expect(200) .expect(200)
expect(res.body.permissionId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.path).toEqual(permission.path) expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type) expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting) expect(res.body.setting).toEqual(permission.setting)
@@ -122,11 +123,11 @@ describe('permission', () => {
.send({ .send({
...permission, ...permission,
principalType: 'group', principalType: 'group',
principalId: dbGroup.groupId principalId: dbGroup.uid
}) })
.expect(200) .expect(200)
expect(res.body.permissionId).toBeTruthy() expect(res.body.uid).toBeTruthy()
expect(res.body.path).toEqual(permission.path) expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type) expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting) expect(res.body.setting).toEqual(permission.setting)
@@ -144,7 +145,7 @@ describe('permission', () => {
}) })
it('should respond with Unauthorized if access token is not of an admin account', async () => { it('should respond with Unauthorized if access token is not of an admin account', async () => {
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const res = await request(app) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
@@ -281,17 +282,19 @@ describe('permission', () => {
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
it('should respond with Bad Request if principalId is not a number', async () => { it('should respond with Bad Request if principalId is not a string of 24 hex characters', async () => {
const res = await request(app) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
...permission, ...permission,
principalId: 'someCharacters' principalId: randomBytes(10).toString('hex')
}) })
.expect(400) .expect(400)
expect(res.text).toEqual('"principalId" must be a number') expect(res.text).toEqual(
'"principalId" length must be 24 characters long'
)
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -307,7 +310,7 @@ describe('permission', () => {
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
...permission, ...permission,
principalId: adminUser.id principalId: adminUser.uid
}) })
.expect(400) .expect(400)
@@ -321,7 +324,7 @@ describe('permission', () => {
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
...permission, ...permission,
principalId: 123 principalId: randomBytes(12).toString('hex')
}) })
.expect(404) .expect(404)
@@ -336,7 +339,7 @@ describe('permission', () => {
.send({ .send({
...permission, ...permission,
principalType: 'group', principalType: 'group',
principalId: 123 principalId: randomBytes(12).toString('hex')
}) })
.expect(404) .expect(404)
@@ -347,13 +350,13 @@ describe('permission', () => {
it('should respond with Conflict (409) if permission already exists', async () => { it('should respond with Conflict (409) if permission already exists', async () => {
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
principalId: dbUser.id principalId: dbUser.uid
}) })
const res = await request(app) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.id }) .send({ ...permission, principalId: dbUser.uid })
.expect(409) .expect(409)
expect(res.text).toEqual( expect(res.text).toEqual(
@@ -368,7 +371,7 @@ describe('permission', () => {
beforeAll(async () => { beforeAll(async () => {
dbPermission = await permissionController.createPermission({ dbPermission = await permissionController.createPermission({
...permission, ...permission,
principalId: dbUser.id principalId: dbUser.uid
}) })
}) })
@@ -378,7 +381,7 @@ describe('permission', () => {
it('should respond with updated permission', async () => { it('should respond with updated permission', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) .patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ setting: PermissionSettingForRoute.deny }) .send({ setting: PermissionSettingForRoute.deny })
.expect(200) .expect(200)
@@ -388,7 +391,7 @@ describe('permission', () => {
it('should respond with Unauthorized if access token is not present', async () => { it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) .patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.send() .send()
.expect(401) .expect(401)
@@ -403,7 +406,7 @@ describe('permission', () => {
}) })
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) .patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(401) .expect(401)
@@ -414,7 +417,7 @@ describe('permission', () => {
it('should respond with Bad Request if setting is missing', async () => { it('should respond with Bad Request if setting is missing', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) .patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(400) .expect(400)
@@ -425,7 +428,7 @@ describe('permission', () => {
it('should respond with Bad Request if setting is invalid', async () => { it('should respond with Bad Request if setting is invalid', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) .patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
setting: 'invalid' setting: 'invalid'
@@ -437,8 +440,9 @@ describe('permission', () => {
}) })
it('should respond with not found (404) if permission with provided id does not exist', async () => { it('should respond with not found (404) if permission with provided id does not exist', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.patch('/SASjsApi/permission/123') .patch(`/SASjsApi/permission/${hexValue}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
setting: PermissionSettingForRoute.deny setting: PermissionSettingForRoute.deny
@@ -454,10 +458,10 @@ describe('permission', () => {
it('should delete permission', async () => { it('should delete permission', async () => {
const dbPermission = await permissionController.createPermission({ const dbPermission = await permissionController.createPermission({
...permission, ...permission,
principalId: dbUser.id principalId: dbUser.uid
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/permission/${dbPermission?.permissionId}`) .delete(`/SASjsApi/permission/${dbPermission?.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -466,8 +470,10 @@ describe('permission', () => {
}) })
it('should respond with not found (404) if permission with provided id does not exists', async () => { it('should respond with not found (404) if permission with provided id does not exists', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.delete('/SASjsApi/permission/123') .delete(`/SASjsApi/permission/${hexValue}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -481,12 +487,12 @@ describe('permission', () => {
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/test-1', path: '/test-1',
principalId: dbUser.id principalId: dbUser.uid
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/test-2', path: '/test-2',
principalId: dbUser.id principalId: dbUser.uid
}) })
}) })
@@ -505,12 +511,12 @@ describe('permission', () => {
...user, ...user,
username: 'get' + user.username username: 'get' + user.username
}) })
const accessToken = await generateAndSaveToken(nonAdminUser.id) const accessToken = await generateAndSaveToken(nonAdminUser.uid)
await permissionController.createPermission({ await permissionController.createPermission({
path: '/test-1', path: '/test-1',
type: PermissionType.route, type: PermissionType.route,
principalType: PrincipalType.user, principalType: PrincipalType.user,
principalId: nonAdminUser.id, principalId: nonAdminUser.uid,
setting: PermissionSettingForRoute.grant setting: PermissionSettingForRoute.grant
}) })
@@ -531,7 +537,7 @@ describe('permission', () => {
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/deploy', path: '/SASjsApi/drive/deploy',
principalId: dbUser.id principalId: dbUser.uid
}) })
}) })
@@ -551,7 +557,7 @@ describe('permission', () => {
}) })
it('should create files in SASJS drive', async () => { it('should create files in SASJS drive', async () => {
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
await request(app) await request(app)
.get('/SASjsApi/drive/deploy') .get('/SASjsApi/drive/deploy')
@@ -561,7 +567,7 @@ describe('permission', () => {
}) })
it('should respond unauthorized', async () => { it('should respond unauthorized', async () => {
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
await request(app) await request(app)
.get('/SASjsApi/drive/deploy/upload') .get('/SASjsApi/drive/deploy/upload')
@@ -577,10 +583,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => { ): Promise<string> => {
const dbUser = await userController.createUser(someUser ?? adminUser) const dbUser = await userController.createUser(someUser ?? adminUser)
return generateAndSaveToken(dbUser.id) return generateAndSaveToken(dbUser.uid)
} }
const generateAndSaveToken = async (userId: number) => { const generateAndSaveToken = async (userId: string) => {
const adminAccessToken = generateAccessToken({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId

View File

@@ -25,7 +25,7 @@ import {
SASSessionController SASSessionController
} from '../../../controllers/internal' } from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram' import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session, SessionState } from '../../../types' import { Session } from '../../../types'
const clientId = 'someclientID' const clientId = 'someclientID'
@@ -58,12 +58,12 @@ describe('stp', () => {
mongoServer = await MongoMemoryServer.create() mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri()) con = await mongoose.connect(mongoServer.getUri())
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
accessToken = await generateAndSaveToken(dbUser.id) accessToken = await generateAndSaveToken(dbUser.uid)
await permissionController.createPermission({ await permissionController.createPermission({
path: '/SASjsApi/stp/execute', path: '/SASjsApi/stp/execute',
type: PermissionType.route, type: PermissionType.route,
principalType: PrincipalType.user, principalType: PrincipalType.user,
principalId: dbUser.id, principalId: dbUser.uid,
setting: PermissionSettingForRoute.grant setting: PermissionSettingForRoute.grant
}) })
}) })
@@ -456,7 +456,7 @@ const makeRequestAndAssert = async (
) )
} }
const generateAndSaveToken = async (userId: number) => { const generateAndSaveToken = async (userId: string) => {
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId, clientId,
userId userId
@@ -493,7 +493,10 @@ const mockedGetSession = async () => {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
state: SessionState.pending, ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder

View File

@@ -1,3 +1,4 @@
import { randomBytes } from 'crypto'
import { Express } from 'express' import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose' import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server' import { MongoMemoryServer } from 'mongodb-memory-server'
@@ -101,9 +102,9 @@ describe('user', () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId, clientId,
userId: dbUser.id userId: dbUser.uid
}) })
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken') await saveTokensInDB(dbUser.uid, clientId, accessToken, 'refreshToken')
const res = await request(app) const res = await request(app)
.post('/SASjsApi/user') .post('/SASjsApi/user')
@@ -187,7 +188,7 @@ describe('user', () => {
const newDisplayName = 'My new display Name' const newDisplayName = 'My new display Name'
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser.id}`) .patch(`/SASjsApi/user/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName }) .send({ ...user, displayName: newDisplayName })
.expect(200) .expect(200)
@@ -200,11 +201,11 @@ describe('user', () => {
it('should respond with updated user when user himself requests', async () => { it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const newDisplayName = 'My new display Name' const newDisplayName = 'My new display Name'
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser.id}`) .patch(`/SASjsApi/user/${dbUser.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ .send({
displayName: newDisplayName, displayName: newDisplayName,
@@ -221,11 +222,11 @@ describe('user', () => {
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => { it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const newDisplayName = 'My new display Name' const newDisplayName = 'My new display Name'
await request(app) await request(app)
.patch(`/SASjsApi/user/${dbUser.id}`) .patch(`/SASjsApi/user/${dbUser.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName }) .send({ ...user, displayName: newDisplayName })
.expect(400) .expect(400)
@@ -277,10 +278,10 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.id) const accessToken = await generateAndSaveToken(dbUser2.uid)
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.id}`) .patch(`/SASjsApi/user/${dbUser1.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send(user) .send(user)
.expect(401) .expect(401)
@@ -297,7 +298,7 @@ describe('user', () => {
}) })
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.id}`) .patch(`/SASjsApi/user/${dbUser1.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username }) .send({ username: dbUser2.username })
.expect(409) .expect(409)
@@ -325,7 +326,7 @@ describe('user', () => {
it('should respond with updated user when user himself requests', async () => { it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const newDisplayName = 'My new display Name' const newDisplayName = 'My new display Name'
const res = await request(app) const res = await request(app)
@@ -346,7 +347,7 @@ describe('user', () => {
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => { it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const newDisplayName = 'My new display Name' const newDisplayName = 'My new display Name'
await request(app) await request(app)
@@ -372,10 +373,10 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.id) const accessToken = await generateAndSaveToken(dbUser2.uid)
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.id}`) .patch(`/SASjsApi/user/${dbUser1.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send(user) .send(user)
.expect(401) .expect(401)
@@ -418,7 +419,7 @@ describe('user', () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.id}`) .delete(`/SASjsApi/user/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -428,10 +429,10 @@ describe('user', () => {
it('should respond with OK when user himself requests', async () => { it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.id}`) .delete(`/SASjsApi/user/${dbUser.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ password: user.password }) .send({ password: user.password })
.expect(200) .expect(200)
@@ -441,10 +442,10 @@ describe('user', () => {
it('should respond with Bad Request when user himself requests and password is missing', async () => { it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.id}`) .delete(`/SASjsApi/user/${dbUser.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(400) .expect(400)
@@ -469,10 +470,10 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.id) const accessToken = await generateAndSaveToken(dbUser2.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser1.id}`) .delete(`/SASjsApi/user/${dbUser1.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send(user) .send(user)
.expect(401) .expect(401)
@@ -483,10 +484,10 @@ describe('user', () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => { it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.id}`) .delete(`/SASjsApi/user/${dbUser.uid}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' }) .send({ password: 'incorrectpassword' })
.expect(401) .expect(401)
@@ -510,7 +511,7 @@ describe('user', () => {
it('should respond with OK when user himself requests', async () => { it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`) .delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -523,7 +524,7 @@ describe('user', () => {
it('should respond with Bad Request when user himself requests and password is missing', async () => { it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`) .delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -551,7 +552,7 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.id) const accessToken = await generateAndSaveToken(dbUser2.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`) .delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
@@ -565,7 +566,7 @@ describe('user', () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => { it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id) const accessToken = await generateAndSaveToken(dbUser.uid)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`) .delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -592,7 +593,7 @@ describe('user', () => {
it('should respond with user autoExec when same user requests', async () => { it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.id const userId = dbUser.uid
const accessToken = await generateAndSaveToken(userId) const accessToken = await generateAndSaveToken(userId)
const res = await request(app) const res = await request(app)
@@ -611,7 +612,7 @@ describe('user', () => {
it('should respond with user autoExec when admin user requests', async () => { it('should respond with user autoExec when admin user requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.id const userId = dbUser.uid
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${userId}`) .get(`/SASjsApi/user/${userId}`)
@@ -634,7 +635,7 @@ describe('user', () => {
}) })
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.id const userId = dbUser.uid
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${userId}`) .get(`/SASjsApi/user/${userId}`)
@@ -652,7 +653,7 @@ describe('user', () => {
it('should respond with user along with associated groups', async () => { it('should respond with user along with associated groups', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.id const userId = dbUser.uid
const accessToken = await generateAndSaveToken(userId) const accessToken = await generateAndSaveToken(userId)
const group = { const group = {
@@ -661,7 +662,7 @@ describe('user', () => {
} }
const groupController = new GroupController() const groupController = new GroupController()
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id) await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${userId}`) .get(`/SASjsApi/user/${userId}`)
@@ -690,8 +691,10 @@ describe('user', () => {
it('should respond with Not Found if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
await controller.createUser(user) await controller.createUser(user)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.get('/SASjsApi/user/1234') .get(`/SASjsApi/user/${hexValue}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -703,7 +706,7 @@ describe('user', () => {
describe('by username', () => { describe('by username', () => {
it('should respond with user autoExec when same user requests', async () => { it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.id const userId = dbUser.uid
const accessToken = await generateAndSaveToken(userId) const accessToken = await generateAndSaveToken(userId)
const res = await request(app) const res = await request(app)
@@ -803,13 +806,13 @@ describe('user', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
id: expect.anything(), uid: expect.anything(),
username: adminUser.username, username: adminUser.username,
displayName: adminUser.displayName, displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin isAdmin: adminUser.isAdmin
}, },
{ {
id: expect.anything(), uid: expect.anything(),
username: user.username, username: user.username,
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin isAdmin: user.isAdmin
@@ -831,13 +834,13 @@ describe('user', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
id: expect.anything(), uid: expect.anything(),
username: adminUser.username, username: adminUser.username,
displayName: adminUser.displayName, displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin isAdmin: adminUser.isAdmin
}, },
{ {
id: expect.anything(), uid: expect.anything(),
username: 'randomUser', username: 'randomUser',
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin isAdmin: user.isAdmin
@@ -859,10 +862,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => { ): Promise<string> => {
const dbUser = await controller.createUser(someUser ?? adminUser) const dbUser = await controller.createUser(someUser ?? adminUser)
return generateAndSaveToken(dbUser.id) return generateAndSaveToken(dbUser.uid)
} }
const generateAndSaveToken = async (userId: number) => { const generateAndSaveToken = async (userId: string) => {
const adminAccessToken = generateAccessToken({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId

View File

@@ -145,7 +145,7 @@ describe('web', () => {
expect(res.body.loggedIn).toBeTruthy() expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({ expect(res.body.user).toEqual({
id: expect.any(Number), id: expect.any(String),
username: user.username, username: user.username,
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,

View File

@@ -1,8 +1,5 @@
import express from 'express' import express from 'express'
import { import { executeProgramRawValidation } from '../../utils'
executeProgramRawValidation,
triggerProgramValidation
} from '../../utils'
import { STPController } from '../../controllers/' import { STPController } from '../../controllers/'
import { FileUploadController } from '../../controllers/internal' import { FileUploadController } from '../../controllers/internal'
@@ -16,11 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
if (error) return res.status(400).send(error.details[0].message) if (error) return res.status(400).send(error.details[0].message)
try { try {
const response = await controller.executeGetRequest( const response = await controller.executeGetRequest(req, query._program)
req,
query._program,
query._debug
)
if (response instanceof Buffer) { if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders) res.writeHead(200, (req as any).sasHeaders)
@@ -72,28 +65,4 @@ stpRouter.post(
} }
) )
stpRouter.post('/trigger', async (req, res) => {
const { error, value: query } = triggerProgramValidation(req.query)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerProgram(
req,
query._program,
query._debug,
query.expiresAfterMins
)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default stpRouter export default stpRouter

View File

@@ -9,6 +9,7 @@ import {
deleteUserValidation, deleteUserValidation,
getUserValidation, getUserValidation,
registerUserValidation, registerUserValidation,
uidValidation,
updateUserValidation updateUserValidation
} from '../../utils' } from '../../utils'
@@ -56,12 +57,15 @@ userRouter.get(
} }
) )
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => { userRouter.get('/:uid', authenticateAccessToken, async (req, res) => {
const { userId } = req.params const { error, value: params } = uidValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
const { uid } = params
const controller = new UserController() const controller = new UserController()
try { try {
const response = await controller.getUser(req, parseInt(userId)) const response = await controller.getUser(req, uid)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -97,12 +101,16 @@ userRouter.patch(
) )
userRouter.patch( userRouter.patch(
'/:userId', '/:uid',
authenticateAccessToken, authenticateAccessToken,
verifyAdminIfNeeded, verifyAdminIfNeeded,
async (req, res) => { async (req, res) => {
const { user } = req const { user } = req
const { userId } = req.params
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
// only an admin can update `isActive` and `isAdmin` fields // only an admin can update `isActive` and `isAdmin` fields
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin) const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
@@ -110,7 +118,7 @@ userRouter.patch(
const controller = new UserController() const controller = new UserController()
try { try {
const response = await controller.updateUser(parseInt(userId), body) const response = await controller.updateUser(uid, body)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -147,12 +155,16 @@ userRouter.delete(
) )
userRouter.delete( userRouter.delete(
'/:userId', '/:uid',
authenticateAccessToken, authenticateAccessToken,
verifyAdminIfNeeded, verifyAdminIfNeeded,
async (req, res) => { async (req, res) => {
const { user } = req const { user } = req
const { userId } = req.params
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
// only an admin can delete user without providing password // only an admin can delete user without providing password
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin) const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
@@ -160,7 +172,7 @@ userRouter.delete(
const controller = new UserController() const controller = new UserController()
try { try {
await controller.deleteUser(parseInt(userId), data, user!.isAdmin) await controller.deleteUser(uid, data, user!.isAdmin)
res.status(200).send('Account Deleted!') res.status(200).send('Account Deleted!')
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)

View File

@@ -1,4 +1,4 @@
export interface InfoJWT { export interface InfoJWT {
clientId: string clientId: string
userId: number userId: string
} }

View File

@@ -1,6 +1,6 @@
export interface PreProgramVars { export interface PreProgramVars {
username: string username: string
userId: number userId: string
displayName: string displayName: string
serverUrl: string serverUrl: string
httpHeaders: string[] httpHeaders: string[]

View File

@@ -1,5 +1,5 @@
export interface RequestUser { export interface RequestUser {
userId: number userId: string
clientId: string clientId: string
username: string username: string
displayName: string displayName: string

View File

@@ -1,16 +1,11 @@
export enum SessionState {
initialising = 'initialising', // session is initialising and not ready to be used yet
pending = 'pending', // session is ready to be used
running = 'running', // session is in use
completed = 'completed', // session is completed and can be destroyed
failed = 'failed' // session failed
}
export interface Session { export interface Session {
id: string id: string
state: SessionState ready: boolean
creationTimeStamp: string creationTimeStamp: string
deathTimeStamp: string deathTimeStamp: string
path: string path: string
expiresAfterMins?: { mins: number; used: boolean } inUse: boolean
failureReason?: string consumed: boolean
completed: boolean
crashed?: string
} }

4
api/src/utils/crypto.ts Normal file
View File

@@ -0,0 +1,4 @@
import { randomBytes } from 'crypto'
export const randomBytesHexString = (bytesCount: number) =>
randomBytes(bytesCount).toString('hex')

View File

@@ -22,7 +22,7 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
//So this is workaround. //So this is workaround.
return { return {
username: user ? user.username : 'demo', username: user ? user.username : 'demo',
userId: user ? user.userId : 0, userId: user ? user.userId : 'demoId',
displayName: user ? user.displayName : 'demo', displayName: user ? user.displayName : 'demo',
serverUrl: protocol + host, serverUrl: protocol + host,
httpHeaders httpHeaders

View File

@@ -1,15 +0,0 @@
import Counter from '../model/Counter'
export const getSequenceNextValue = async (seqName: string) => {
const seqDoc = await Counter.findOne({ id: seqName })
if (!seqDoc) {
await Counter.create({ id: seqName, seq: 1 })
return 1
}
seqDoc.seq += 1
await seqDoc.save()
return seqDoc.seq
}

View File

@@ -4,7 +4,7 @@ import User from '../model/User'
const isValidToken = async ( const isValidToken = async (
token: string, token: string,
key: string, key: string,
userId: number, userId: string,
clientId: string clientId: string
) => { ) => {
const promise = new Promise<boolean>((resolve, reject) => const promise = new Promise<boolean>((resolve, reject) =>
@@ -22,8 +22,8 @@ const isValidToken = async (
return await promise.then(() => true).catch(() => false) return await promise.then(() => true).catch(() => false)
} }
export const getTokensFromDB = async (userId: number, clientId: string) => { export const getTokensFromDB = async (userId: string, clientId: string) => {
const user = await User.findOne({ id: userId }) const user = await User.findOne({ _id: userId })
if (!user) return if (!user) return
const currentTokenObj = user.tokens.find( const currentTokenObj = user.tokens.find(

View File

@@ -2,6 +2,7 @@ export * from './appStreamConfig'
export * from './connectDB' export * from './connectDB'
export * from './copySASjsCore' export * from './copySASjsCore'
export * from './createWeboutSasFile' export * from './createWeboutSasFile'
export * from './crypto'
export * from './desktopAutoExec' export * from './desktopAutoExec'
export * from './extractHeaders' export * from './extractHeaders'
export * from './extractName' export * from './extractName'
@@ -14,7 +15,6 @@ export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './getPreProgramVariables' export * from './getPreProgramVariables'
export * from './getRunTimeAndFilePath' export * from './getRunTimeAndFilePath'
export * from './getSequenceNextValue'
export * from './getServerUrl' export * from './getServerUrl'
export * from './getTokensFromDB' export * from './getTokensFromDB'
export * from './instantiateLogger' export * from './instantiateLogger'

View File

@@ -22,7 +22,7 @@ export const isPublicRoute = async (req: Request): Promise<boolean> => {
} }
export const publicUser: RequestUser = { export const publicUser: RequestUser = {
userId: 0, userId: 'public_user_id',
clientId: 'public_app', clientId: 'public_app',
username: 'publicUser', username: 'publicUser',
displayName: 'Public User', displayName: 'Public User',

View File

@@ -1,7 +1,7 @@
import User from '../model/User' import User from '../model/User'
export const removeTokensInDB = async (userId: number, clientId: string) => { export const removeTokensInDB = async (userId: string, clientId: string) => {
const user = await User.findOne({ id: userId }) const user = await User.findOne({ _id: userId })
if (!user) return if (!user) return
const tokenObjIndex = user.tokens.findIndex( const tokenObjIndex = user.tokens.findIndex(

View File

@@ -1,12 +1,12 @@
import User from '../model/User' import User from '../model/User'
export const saveTokensInDB = async ( export const saveTokensInDB = async (
userId: number, userId: string,
clientId: string, clientId: string,
accessToken: string, accessToken: string,
refreshToken: string refreshToken: string
) => { ) => {
const user = await User.findOne({ id: userId }) const user = await User.findOne({ _id: userId })
if (!user) return if (!user) return
const currentTokenObj = user.tokens.find( const currentTokenObj = user.tokens.find(

View File

@@ -82,7 +82,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
} }
export const ALL_USERS_GROUP = { export const ALL_USERS_GROUP = {
name: 'AllUsers', name: 'all-users',
description: 'Group contains all users' description: 'Group contains all users'
} }

View File

@@ -1,31 +1,9 @@
import path from 'path' import path from 'path'
import { import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
createFolder,
getAbsolutePath,
getRealPath,
fileExists
} from '@sasjs/utils'
import dotenv from 'dotenv'
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.' import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
export const setProcessVariables = async () => { export const setProcessVariables = async () => {
const { execPath } = process
// Check if execPath ends with 'api-macos' to determine executable for MacOS.
// This is needed to fix picking .env file issue in MacOS executable.
if (execPath) {
const envPathSplitted = execPath.split(path.sep)
if (envPathSplitted.pop() === 'api-macos') {
const envPath = path.join(envPathSplitted.join(path.sep), '.env')
// Override environment variables from envPath if file exists
if (await fileExists(envPath)) {
dotenv.config({ path: envPath, override: true })
}
}
}
const { MODE, RUN_TIMES } = process.env const { MODE, RUN_TIMES } = process.env
if (MODE === ModeType.Server) { if (MODE === ModeType.Server) {
@@ -43,7 +21,6 @@ export const setProcessVariables = async () => {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root') process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive') process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
return return
} }
@@ -64,9 +41,7 @@ export const setProcessVariables = async () => {
const { SASJS_ROOT } = process.env const { SASJS_ROOT } = process.env
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd()) const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
await createFolder(absPath) await createFolder(absPath)
process.sasjsRoot = getRealPath(absPath) process.sasjsRoot = getRealPath(absPath)
const { DRIVE_LOCATION } = process.env const { DRIVE_LOCATION } = process.env
@@ -74,7 +49,6 @@ export const setProcessVariables = async () => {
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'), DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
process.cwd() process.cwd()
) )
await createFolder(absDrivePath) await createFolder(absDrivePath)
process.driveLoc = getRealPath(absDrivePath) process.driveLoc = getRealPath(absDrivePath)
@@ -83,9 +57,7 @@ export const setProcessVariables = async () => {
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'), LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
process.cwd() process.cwd()
) )
await createFolder(absLogsPath) await createFolder(absLogsPath)
process.logsLoc = getRealPath(absLogsPath) process.logsLoc = getRealPath(absLogsPath)
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784' process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'

View File

@@ -51,8 +51,9 @@ export const generateFileUploadSasCode = async (
let fileCount = 0 let fileCount = 0
const uploadedFiles: UploadedFiles[] = [] const uploadedFiles: UploadedFiles[] = []
const sasSessionFolderList: string[] = const sasSessionFolderList: string[] = await listFilesInFolder(
await listFilesInFolder(sasSessionFolder) sasSessionFolder
)
sasSessionFolderList.forEach((fileName) => { sasSessionFolderList.forEach((fileName) => {
let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount
fileCountString = fileCount < 10 ? '00' + fileCount : fileCount fileCountString = fileCount < 10 ? '00' + fileCount : fileCount

View File

@@ -12,6 +12,11 @@ const groupnameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i export const blockFileRegex = /\.(exe|sh|htaccess)$/i
export const uidValidation = (data: any) =>
Joi.object({
uid: Joi.string().length(24).hex().required()
}).validate(data)
export const getUserValidation = (data: any): Joi.ValidationResult => export const getUserValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
username: usernameSchema.required() username: usernameSchema.required()
@@ -113,7 +118,7 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
principalType: Joi.string() principalType: Joi.string()
.required() .required()
.valid(...Object.values(PrincipalType)), .valid(...Object.values(PrincipalType)),
principalId: Joi.number().required() principalId: Joi.string().length(24).hex().required()
}).validate(data) }).validate(data)
export const updatePermissionValidation = (data: any): Joi.ValidationResult => export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
@@ -178,31 +183,9 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
runTime: Joi.string().valid(...process.runTimes) runTime: Joi.string().valid(...process.runTimes)
}).validate(data) }).validate(data)
export const triggerCodeValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required(),
runTime: Joi.string().valid(...process.runTimes),
expiresAfterMins: Joi.number().greater(0)
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult => export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
_program: Joi.string().required(), _program: Joi.string().required()
_debug: Joi.number()
}) })
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number())) .pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
.validate(data) .validate(data)
export const triggerProgramValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_program: Joi.string().required(),
_debug: Joi.number(),
expiresAfterMins: Joi.number().greater(0)
})
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
.validate(data)
export const sessionIdValidation = (data: any): Joi.ValidationResult =>
Joi.object({
sessionId: Joi.string().required()
}).validate(data)

View File

@@ -4,7 +4,7 @@ import { RequestUser } from '../types'
export const fetchLatestAutoExec = async ( export const fetchLatestAutoExec = async (
reqUser: RequestUser reqUser: RequestUser
): Promise<RequestUser | undefined> => { ): Promise<RequestUser | undefined> => {
const dbUser = await User.findOne({ id: reqUser.userId }) const dbUser = await User.findOne({ _id: reqUser.userId })
if (!dbUser) return undefined if (!dbUser) return undefined
@@ -21,12 +21,12 @@ export const fetchLatestAutoExec = async (
} }
export const verifyTokenInDB = async ( export const verifyTokenInDB = async (
userId: number, userId: string,
clientId: string, clientId: string,
token: string, token: string,
tokenType: 'accessToken' | 'refreshToken' tokenType: 'accessToken' | 'refreshToken'
): Promise<RequestUser | undefined> => { ): Promise<RequestUser | undefined> => {
const dbUser = await User.findOne({ id: userId }) const dbUser = await User.findOne({ _id: userId })
if (!dbUser) return undefined if (!dbUser) return undefined

11680
package-lock.json generated

File diff suppressed because it is too large Load Diff

3538
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^12.20.28", "@types/node": "^12.20.28",
"@types/react": "^17.0.27", "@types/react": "^17.0.27",
"axios": "^1.12.2", "axios": "^0.24.0",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
@@ -62,7 +62,7 @@
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^4.5.2", "typescript": "^4.5.2",
"typescript-plugin-css-modules": "^5.0.1", "typescript-plugin-css-modules": "^5.0.1",
"webpack": "^5.101.3", "webpack": "5.64.3",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2",
"webpack-dev-server": "4.7.4" "webpack-dev-server": "4.7.4"
}, },

View File

@@ -3,11 +3,12 @@ import Snackbar from '@mui/material/Snackbar'
import MuiAlert, { AlertProps } from '@mui/material/Alert' import MuiAlert, { AlertProps } from '@mui/material/Alert'
import Slide, { SlideProps } from '@mui/material/Slide' import Slide, { SlideProps } from '@mui/material/Slide'
const Alert = React.forwardRef<HTMLDivElement, AlertProps>( const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
function Alert(props, ref) { props,
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} /> ref
} ) {
) return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
})
const Transition = (props: SlideProps) => { const Transition = (props: SlideProps) => {
return <Slide {...props} direction="up" /> return <Slide {...props} direction="up" />

View File

@@ -99,8 +99,8 @@ const AddPermissionModal = ({
principalType: principalType.toLowerCase(), principalType: principalType.toLowerCase(),
principalId: principalId:
principalType.toLowerCase() === 'user' principalType.toLowerCase() === 'user'
? userPrincipal?.id ? userPrincipal?.uid
: groupPrincipal?.groupId : groupPrincipal?.uid
} }
permissions.push(addPermissionPayload) permissions.push(addPermissionPayload)

View File

@@ -61,7 +61,7 @@ const PermissionTable = ({
</TableHead> </TableHead>
<TableBody> <TableBody>
{permissions.map((permission) => ( {permissions.map((permission) => (
<TableRow key={permission.permissionId}> <TableRow key={permission.uid}>
<BootstrapTableCell>{permission.path}</BootstrapTableCell> <BootstrapTableCell>{permission.path}</BootstrapTableCell>
<BootstrapTableCell>{permission.type}</BootstrapTableCell> <BootstrapTableCell>{permission.type}</BootstrapTableCell>
<BootstrapTableCell> <BootstrapTableCell>

View File

@@ -69,7 +69,7 @@ const useAddPermission = () => {
for (const permission of updatingPermissions) { for (const permission of updatingPermissions) {
await axios await axios
.patch(`/SASjsApi/permission/${permission.permissionId}`, { .patch(`/SASjsApi/permission/${permission.uid}`, {
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant' setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
}) })
.then((res) => { .then((res) => {

View File

@@ -24,7 +24,7 @@ const useDeletePermissionModal = () => {
setDeleteConfirmationModalOpen(false) setDeleteConfirmationModalOpen(false)
setIsLoading(true) setIsLoading(true)
axios axios
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`) .delete(`/SASjsApi/permission/${selectedPermission?.uid}`)
.then((res: any) => { .then((res: any) => {
fetchPermissions() fetchPermissions()
setSnackbarMessage('Permission deleted!') setSnackbarMessage('Permission deleted!')

View File

@@ -62,21 +62,17 @@ const useFilterPermissions = () => {
: permissions : permissions
let filteredArray = uriFilteredPermissions.filter((permission) => let filteredArray = uriFilteredPermissions.filter((permission) =>
principalFilteredPermissions.some( principalFilteredPermissions.some((item) => item.uid === permission.uid)
(item) => item.permissionId === permission.permissionId
)
) )
filteredArray = filteredArray.filter((permission) => filteredArray = filteredArray.filter((permission) =>
principalTypeFilteredPermissions.some( principalTypeFilteredPermissions.some(
(item) => item.permissionId === permission.permissionId (item) => item.uid === permission.uid
) )
) )
filteredArray = filteredArray.filter((permission) => filteredArray = filteredArray.filter((permission) =>
settingFilteredPermissions.some( settingFilteredPermissions.some((item) => item.uid === permission.uid)
(item) => item.permissionId === permission.permissionId
)
) )
setFilteredPermissions(filteredArray) setFilteredPermissions(filteredArray)

View File

@@ -24,7 +24,7 @@ const useUpdatePermissionModal = () => {
setUpdatePermissionModalOpen(false) setUpdatePermissionModalOpen(false)
setIsLoading(true) setIsLoading(true)
axios axios
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, { .patch(`/SASjsApi/permission/${selectedPermission?.uid}`, {
setting setting
}) })
.then((res: any) => { .then((res: any) => {

View File

@@ -26,18 +26,20 @@ const Profile = () => {
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
useEffect(() => { useEffect(() => {
setIsLoading(true) if (appContext.userId) {
axios setIsLoading(true)
.get(`/SASjsApi/user/${appContext.userId}`) axios
.then((res: any) => { .get(`/SASjsApi/user/${appContext.userId}`)
setUser(res.data) .then((res: any) => {
}) setUser(res.data)
.catch((err) => { })
console.log(err) .catch((err) => {
}) console.log(err)
.finally(() => { })
setIsLoading(false) .finally(() => {
}) setIsLoading(false)
})
}
}, [appContext.userId]) }, [appContext.userId])
const handleChange = (event: any) => { const handleChange = (event: any) => {

View File

@@ -62,7 +62,6 @@ const SASjsEditor = ({
selectedRunTime, selectedRunTime,
showDiff, showDiff,
webout, webout,
printOutput,
Dialog, Dialog,
handleChangeRunTime, handleChangeRunTime,
handleDiffEditorDidMount, handleDiffEditorDidMount,
@@ -154,35 +153,30 @@ const SASjsEditor = ({
> >
<TabList onChange={handleTabChange} centered> <TabList onChange={handleTabChange} centered>
<StyledTab label="Code" value="code" /> <StyledTab label="Code" value="code" />
{log && ( <StyledTab
<StyledTab label={logWithErrorsOrWarnings ? '' : 'log'}
label={logWithErrorsOrWarnings ? '' : 'log'} value="log"
value="log" icon={
icon={ logWithErrorsOrWarnings ? (
logWithErrorsOrWarnings ? ( <LogTabWithIcons log={log as LogObject} />
<LogTabWithIcons log={log as LogObject} /> ) : (
) : ( ''
'' )
) }
} onClick={() => {
onClick={() => { const logWrapper = document.querySelector(`#logWrapper`)
const logWrapper = document.querySelector(`#logWrapper`)
if (logWrapper) logWrapper.scrollTop = 0 if (logWrapper) logWrapper.scrollTop = 0
}} }}
/> />
)} <StyledTab
{webout && ( label={
<StyledTab <Tooltip title="Displays content from the _webout fileref">
label={ <Typography>Webout</Typography>
<Tooltip title="Displays content from the _webout fileref"> </Tooltip>
<Typography>Webout</Typography> }
</Tooltip> value="webout"
} />
value="webout"
/>
)}
{printOutput && <StyledTab label="print" value="printOutput" />}
</TabList> </TabList>
</Box> </Box>
@@ -228,20 +222,11 @@ const SASjsEditor = ({
<LogComponent log={log} selectedRunTime={selectedRunTime} /> <LogComponent log={log} selectedRunTime={selectedRunTime} />
)} )}
</StyledTabPanel> </StyledTabPanel>
{webout && ( <StyledTabPanel value="webout">
<StyledTabPanel value="webout"> <div>
<div> <pre>{webout}</pre>
<pre>{webout}</pre> </div>
</div> </StyledTabPanel>
</StyledTabPanel>
)}
{printOutput && (
<StyledTabPanel value="printOutput">
<div>
<pre>{printOutput}</pre>
</div>
</StyledTabPanel>
)}
</TabContext> </TabContext>
)} )}
<Dialog /> <Dialog />

View File

@@ -7,10 +7,8 @@
border: none; border: none;
outline: none; outline: none;
transition: 0.4s; transition: 0.4s;
box-shadow: box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px,
rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px,
rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
} }
.ChunkDetails { .ChunkDetails {

View File

@@ -39,14 +39,14 @@ const useEditor = ({
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } = const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
useSnackbar() useSnackbar()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [prevFileContent, setPrevFileContent] = useStateWithCallback('') const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
const [fileContent, setFileContent] = useState('') const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState<LogObject | string>() const [log, setLog] = useState<LogObject | string>()
const [webout, setWebout] = useState<string>() const [webout, setWebout] = useState('')
const [printOutput, setPrintOutput] = useState<string>()
const [runTimes, setRunTimes] = useState<string[]>([]) const [runTimes, setRunTimes] = useState<string[]>([])
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType>( const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType | string>(
RunTimeType.SAS ''
) )
const [selectedFileExtension, setSelectedFileExtension] = useState('') const [selectedFileExtension, setSelectedFileExtension] = useState('')
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false) const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
@@ -169,30 +169,25 @@ const useEditor = ({
), ),
runTime: selectedRunTime runTime: selectedRunTime
}) })
.then((res: { data: string }) => { .then((res: any) => {
// INFO: the order of payload parts is set in @sasjs/server/api/src/controllers/internal/Execution.ts
const resDataSplitted = res.data.split(SASJS_LOGS_SEPARATOR)
const webout = resDataSplitted[0]
const log = resDataSplitted[1]
const printOutput = resDataSplitted[2]
if (selectedRunTime === RunTimeType.SAS) { if (selectedRunTime === RunTimeType.SAS) {
const { errors, warnings, logLines } = parseErrorsAndWarnings(log) const { errors, warnings, logLines } = parseErrorsAndWarnings(
res.data.split(SASJS_LOGS_SEPARATOR)[1]
)
const logObject: LogObject = { const log: LogObject = {
body: logLines.join(`\n`), body: logLines.join(`\n`),
errors, errors,
warnings, warnings,
linesCount: logLines.length linesCount: logLines.length
} }
setLog(logObject)
} else {
setLog(log) setLog(log)
} else {
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
} }
setWebout(webout) setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
setPrintOutput(printOutput)
setTab('log') setTab('log')
// Scroll to bottom of log // Scroll to bottom of log
@@ -340,7 +335,6 @@ const useEditor = ({
selectedRunTime, selectedRunTime,
showDiff, showDiff,
webout, webout,
printOutput,
Dialog, Dialog,
handleChangeRunTime, handleChangeRunTime,
handleDiffEditorDidMount, handleDiffEditorDidMount,

View File

@@ -24,39 +24,32 @@ export enum RunTimeType {
interface AppContextProps { interface AppContextProps {
checkingSession: boolean checkingSession: boolean
loggedIn: boolean loggedIn: boolean
setLoggedIn: Dispatch<SetStateAction<boolean>> | null setLoggedIn?: Dispatch<SetStateAction<boolean>>
needsToUpdatePassword: boolean needsToUpdatePassword: boolean
setNeedsToUpdatePassword: Dispatch<SetStateAction<boolean>> | null setNeedsToUpdatePassword?: Dispatch<SetStateAction<boolean>>
userId: number userId?: string
setUserId: Dispatch<SetStateAction<number>> | null setUserId?: Dispatch<SetStateAction<string | undefined>>
username: string username: string
setUsername: Dispatch<SetStateAction<string>> | null setUsername?: Dispatch<SetStateAction<string>>
displayName: string displayName: string
setDisplayName: Dispatch<SetStateAction<string>> | null setDisplayName?: Dispatch<SetStateAction<string>>
isAdmin: boolean isAdmin: boolean
setIsAdmin: Dispatch<SetStateAction<boolean>> | null setIsAdmin?: Dispatch<SetStateAction<boolean>>
mode: ModeType mode: ModeType
runTimes: RunTimeType[] runTimes: RunTimeType[]
logout: (() => void) | null logout?: () => void
} }
export const AppContext = createContext<AppContextProps>({ export const AppContext = createContext<AppContextProps>({
checkingSession: false, checkingSession: false,
loggedIn: false, loggedIn: false,
setLoggedIn: null,
needsToUpdatePassword: false, needsToUpdatePassword: false,
setNeedsToUpdatePassword: null, userId: '',
userId: 0,
setUserId: null,
username: '', username: '',
setUsername: null,
displayName: '', displayName: '',
setDisplayName: null,
isAdmin: false, isAdmin: false,
setIsAdmin: null,
mode: ModeType.Server, mode: ModeType.Server,
runTimes: [], runTimes: []
logout: null
}) })
const AppContextProvider = (props: { children: ReactNode }) => { const AppContextProvider = (props: { children: ReactNode }) => {
@@ -64,7 +57,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const [checkingSession, setCheckingSession] = useState(false) const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false) const [loggedIn, setLoggedIn] = useState(false)
const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false) const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false)
const [userId, setUserId] = useState(0) const [userId, setUserId] = useState<string>()
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('') const [displayName, setDisplayName] = useState('')
const [isAdmin, setIsAdmin] = useState(false) const [isAdmin, setIsAdmin] = useState(false)

View File

@@ -1,15 +1,15 @@
body { body {
margin: 0; margin: 0;
font-family: font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; monospace;
} }
.container { .container {

View File

@@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@@ -6,13 +6,13 @@ export const findExistingPermission = (
) => { ) => {
for (const permission of existingPermissions) { for (const permission of existingPermissions) {
if ( if (
permission.user?.id === newPermission.principalId && permission.user?.uid === newPermission.principalId &&
hasSameCombination(permission, newPermission) hasSameCombination(permission, newPermission)
) )
return permission return permission
if ( if (
permission.group?.groupId === newPermission.principalId && permission.group?.uid === newPermission.principalId &&
hasSameCombination(permission, newPermission) hasSameCombination(permission, newPermission)
) )
return permission return permission
@@ -27,13 +27,13 @@ export const findUpdatingPermission = (
) => { ) => {
for (const permission of existingPermissions) { for (const permission of existingPermissions) {
if ( if (
permission.user?.id === newPermission.principalId && permission.user?.uid === newPermission.principalId &&
hasDifferentSetting(permission, newPermission) hasDifferentSetting(permission, newPermission)
) )
return permission return permission
if ( if (
permission.group?.groupId === newPermission.principalId && permission.group?.uid === newPermission.principalId &&
hasDifferentSetting(permission, newPermission) hasDifferentSetting(permission, newPermission)
) )
return permission return permission

View File

@@ -1,12 +1,12 @@
export interface UserResponse { export interface UserResponse {
id: number uid: string
username: string username: string
displayName: string displayName: string
isAdmin: boolean isAdmin: boolean
} }
export interface GroupResponse { export interface GroupResponse {
groupId: number uid: string
name: string name: string
description: string description: string
} }
@@ -17,7 +17,7 @@ export interface GroupDetailsResponse extends GroupResponse {
} }
export interface PermissionResponse { export interface PermissionResponse {
permissionId: number uid: string
path: string path: string
type: string type: string
setting: string setting: string
@@ -30,7 +30,7 @@ export interface RegisterPermissionPayload {
type: string type: string
setting: string setting: string
principalType: string principalType: string
principalId: number principalId: string
} }
export interface TreeNode { export interface TreeNode {