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

Compare commits

...

33 Commits

Author SHA1 Message Date
Sabir Hassan
6690cafbf7 Merge c43afabe28 into 0838b8112e 2024-10-29 10:35:32 +00:00
semantic-release-bot
0838b8112e chore(release): 0.36.0 [skip ci]
# [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](ffcf193b87))
2024-10-29 10:32:01 +00:00
Yury Shkoda
441f8b7726 Merge pull request #374 from sasjs/issue-373
feat(code): added code/trigger API endpoint
2024-10-29 13:29:08 +03:00
Yury
049a7f4b80 chore(swagger): improved description 2024-10-29 12:02:26 +03:00
Yury
3053c68bdf chore(lint): fixed linting issues 2024-10-29 11:40:44 +03:00
Yury
76750e864d chore(lint): fixed lint issue 2024-10-29 11:30:05 +03:00
Yury
ffcf193b87 feat(code): added code/trigger API endpoint 2024-10-29 11:18:04 +03:00
semantic-release-bot
aa2a1cbe13 chore(release): 0.35.4 [skip ci]
## [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](73d965daf5))
2024-01-15 13:21:15 +00:00
Yury Shkoda
6f2c53555c Merge pull request #372 from sasjs/issue-371
fix(api): fixed env issue in MacOS executable
2024-01-15 16:18:10 +03:00
Yury
73d965daf5 fix(api): fixed env issue in MacOS executable 2024-01-15 15:14:06 +03:00
semantic-release-bot
4f1763db67 chore(release): 0.35.3 [skip ci]
## [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](7e8cbbf377))
2023-11-07 20:48:28 +00:00
Allan Bowe
28222add04 Merge pull request #370 from sasjs/allanbowe-patch-1
fix: enable embedded LFs in JS STP vars
2023-11-07 20:43:16 +00:00
Allan
068edfd6a5 chore: lint fix 2023-11-07 20:39:05 +00:00
Allan Bowe
7e8cbbf377 fix: enable embedded LFs in JS STP vars 2023-11-07 15:51:32 +00:00
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
Allan Bowe
1fc1431442 chore: using GITHUB_TOKEN 2023-08-07 20:11:40 +01:00
semantic-release-bot
3387efbb9a chore(release): 0.35.2 [skip ci]
## [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](9586dbb2d0))
2023-08-07 18:53:12 +00:00
Allan Bowe
e2996b495f Merge pull request #365 from sasjs/swagger-fix
fix: add _debug as optional query param in swagger apis for  stp/execute
2023-08-07 19:48:28 +01:00
Allan
41c627f93a chore: lint fix 2023-08-07 19:39:02 +01:00
Allan Bowe
49f5dc7555 Update swagger.yaml 2023-08-07 19:32:29 +01:00
Allan Bowe
f6e77f99a4 Update swagger.yaml 2023-08-07 19:31:20 +01:00
Allan Bowe
b57dfa429b Update stp.ts 2023-08-07 19:30:09 +01:00
9586dbb2d0 fix: add _debug as optional query param in swagger apis for GET stp/execute 2023-08-07 22:01:52 +05:00
semantic-release-bot
a4f78ab48d chore(release): 0.35.1 [skip ci]
## [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](8940f4dc47))
2023-07-25 06:05:23 +00:00
Yury Shkoda
2f47a2213b Merge pull request #364 from sasjs/log-separator
fix(log-separator): log separator should always wrap log
2023-07-25 09:01:36 +03:00
Yury Shkoda
0f91395fbb lint: fixed linting issues 2023-07-24 18:36:08 +03:00
Yury Shkoda
167b14fed0 docs(log-separator): left comment 2023-07-24 18:29:20 +03:00
Yury Shkoda
8940f4dc47 fix(log-separator): log separator should always wrap log 2023-07-24 18:27:21 +03: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
67 changed files with 1007 additions and 734 deletions

View File

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

View File

@@ -1,3 +1,38 @@
# [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) # [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)

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

16
api/package-lock.json generated
View File

@@ -51,7 +51,7 @@
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"axios": "0.27.2", "axios": "0.27.2",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dotenv": "^10.0.0", "dotenv": "^16.0.1",
"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",
@@ -4788,12 +4788,12 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "10.0.0", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=12"
} }
}, },
"node_modules/duplexer2": { "node_modules/duplexer2": {
@@ -15217,9 +15217,9 @@
} }
}, },
"dotenv": { "dotenv": {
"version": "10.0.0", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
"dev": true "dev": true
}, },
"duplexer2": { "duplexer2": {

View File

@@ -88,7 +88,7 @@
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"axios": "0.27.2", "axios": "0.27.2",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dotenv": "^10.0.0", "dotenv": "^16.0.1",
"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",

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,17 +97,47 @@ components:
properties: properties:
code: code:
type: string type: string
description: 'Code of program' description: 'The code to be executed'
example: '* Code HERE;' example: '* Your Code HERE;'
runTime: runTime:
$ref: '#/components/schemas/RunTimeType' $ref: '#/components/schemas/RunTimeType'
description: 'runtime for program' description: 'The runtime for the code - eg SAS, JS, PY or R'
example: js example: js
required: required:
- code - code
- runTime - runTime
type: object type: object
additionalProperties: false additionalProperties: false
TriggerCodeResponse:
properties:
sessionId:
type: string
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll job status.\nThis session ID should be used to poll job status."
example: '{ sessionId: ''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
@@ -285,9 +314,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:
@@ -295,7 +323,7 @@ components:
isAdmin: isAdmin:
type: boolean type: boolean
required: required:
- id - uid
- username - username
- displayName - displayName
- isAdmin - isAdmin
@@ -303,32 +331,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:
@@ -336,11 +362,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:
@@ -376,9 +402,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:
@@ -390,7 +415,7 @@ components:
$ref: '#/components/schemas/UserResponse' $ref: '#/components/schemas/UserResponse'
type: array type: array
required: required:
- groupId - uid
- name - name
- description - description
- isActive - isActive
@@ -459,9 +484,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:
@@ -473,7 +497,7 @@ components:
group: group:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
required: required:
- permissionId - uid
- path - path
- type - type
- setting - setting
@@ -512,10 +536,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
@@ -534,25 +556,37 @@ 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
ExecutePostRequestPayload: ExecutePostRequestPayload:
@@ -792,7 +826,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 and Log' 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"
tags: tags:
- Code - Code
security: security:
@@ -805,6 +839,30 @@ 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
@@ -1206,7 +1264,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
@@ -1225,7 +1283,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
@@ -1276,7 +1334,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
@@ -1327,7 +1385,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:
@@ -1346,14 +1404,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:
@@ -1365,7 +1421,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
@@ -1379,8 +1435,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
@@ -1406,8 +1461,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
@@ -1432,7 +1486,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
@@ -1451,7 +1505,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
@@ -1467,7 +1521,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
@@ -1489,7 +1543,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:
@@ -1509,12 +1563,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:
@@ -1536,13 +1589,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:
@@ -1554,7 +1606,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
@@ -1565,21 +1617,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:
@@ -1591,8 +1640,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:
@@ -1602,21 +1651,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
@@ -1667,7 +1714,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:
@@ -1687,7 +1734,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
@@ -1701,7 +1748,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:
@@ -1713,7 +1760,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
@@ -1722,14 +1769,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:
@@ -1749,14 +1793,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
@@ -1769,7 +1810,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
@@ -1789,7 +1830,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 URL parameters and 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 additional URL parameters (converted to session variables)\nand 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
@@ -1798,13 +1839,22 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'Location of code in SASjs Drive' description: 'Location of Stored Program 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:
@@ -1863,7 +1913,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,27 +1,55 @@
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 } from './internal' import { ExecutionController, getSessionController } from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
getUserAutoExec, getUserAutoExec,
ModeType, ModeType,
parseLogToArray,
RunTimeType RunTimeType
} from '../utils' } from '../utils'
interface ExecuteCodePayload { interface ExecuteCodePayload {
/** /**
* Code of program * The code to be executed
* @example "* Code HERE;" * @example "* Your Code HERE;"
*/ */
code: string code: string
/** /**
* runtime for program * The runtime for the code - eg SAS, JS, PY or R
* @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 {
/**
* The SessionId is the name of the temporary folder used to store the outputs.
* For SAS, this would be the SASWORK folder. Can be used to poll job status.
* This session ID should be used to poll job status.
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
*/
sessionId: string
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/code') @Route('SASjsApi/code')
@Tags('Code') @Tags('Code')
@@ -44,6 +72,18 @@ 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 (
@@ -76,3 +116,49 @@ const executeCode = async (
} }
} }
} }
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<{ sessionId: string }> => {
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

@@ -128,8 +128,10 @@ export class ExecutionController {
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty // 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) 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(process.logsUUID)
resultParts.push(log) resultParts.push(log)
resultParts.push(process.logsUUID)
if (includePrintOutput && runTime === RunTimeType.SAS) { if (includePrintOutput && runTime === RunTimeType.SAS) {
const printOutputPath = path.join(session.path, 'output.lst') const printOutputPath = path.join(session.path, 'output.lst')
@@ -137,10 +139,7 @@ export class ExecutionController {
? await readFile(printOutputPath) ? await readFile(printOutputPath)
: '' : ''
if (printOutput) { if (printOutput) resultParts.push(printOutput)
resultParts.push(process.logsUUID)
resultParts.push(printOutput)
}
} }
return { return {

View File

@@ -14,8 +14,7 @@ import {
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile, readFile
isWindows
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -190,17 +189,37 @@ ${autoExecContent}`
} }
private scheduleSessionDestroy(session: Session) { private scheduleSessionDestroy(session: Session) {
setTimeout(async () => { setTimeout(
if (session.inUse) { async () => {
// adding 10 more minutes if (session.inUse) {
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000 // adding 10 more minutes
session.deathTimeStamp = newDeathTimeStamp.toString() const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session) this.scheduleSessionDestroy(session)
} else { } else {
await this.deleteSession(session) const { expiresAfterMins } = session
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100) // delay session destroy if expiresAfterMins present
if (expiresAfterMins && !expiresAfterMins.used) {
// 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
)
} }
} }

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

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

@@ -2,8 +2,9 @@ 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'
interface SessionResponse extends UserResponse { interface SessionResponse extends Omit<UserResponse, 'uid'> {
needsToUpdatePassword: boolean id: string
needsToUpdatePassword?: boolean
} }
@Security('bearerAuth') @Security('bearerAuth')
@@ -14,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(

View File

@@ -7,6 +7,7 @@ import {
getRunTimeAndFilePath getRunTimeAndFilePath
} from '../utils' } from '../utils'
import { MulterFile } from '../types/Upload' import { MulterFile } from '../types/Upload'
import { debug } from 'console'
interface ExecutePostRequestPayload { interface ExecutePostRequestPayload {
/** /**
@@ -23,20 +24,30 @@ export class STPController {
/** /**
* Trigger a Stored Program using the _program URL parameter. * Trigger a Stored Program using the _program URL parameter.
* *
* Accepts URL parameters and file uploads. For more details, see docs: * Accepts additional URL parameters (converted to session variables)
* 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 code in SASjs Drive * @param _program Location of Stored Program 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> {
const vars = request.query as ExecutionVars let vars = request.query as ExecutionVars
if (_debug) {
vars = {
...vars,
_debug
}
}
return execute(request, _program, vars) return execute(request, _program, vars)
} }

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,40 +39,46 @@ 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()
}) })
}) })

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 } from '../../utils' import { runCodeValidation, triggerCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/' import { CodeController } from '../../controllers/'
const runRouter = express.Router() const runRouter = express.Router()
@@ -28,4 +28,22 @@ 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

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

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

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

@@ -13,7 +13,11 @@ 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(req, query._program) const response = await controller.executeGetRequest(
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)

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

@@ -8,4 +8,5 @@ export interface Session {
consumed: boolean consumed: boolean
completed: boolean completed: boolean
crashed?: string crashed?: string
expiresAfterMins?: { mins: number; used: boolean }
} }

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,9 +1,31 @@
import path from 'path' import path from 'path'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils' import {
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) {
@@ -21,6 +43,7 @@ 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
} }
@@ -41,7 +64,9 @@ 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
@@ -49,6 +74,7 @@ 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)
@@ -57,7 +83,9 @@ 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,9 +51,8 @@ export const generateFileUploadSasCode = async (
let fileCount = 0 let fileCount = 0
const uploadedFiles: UploadedFiles[] = [] const uploadedFiles: UploadedFiles[] = []
const sasSessionFolderList: string[] = await listFilesInFolder( const sasSessionFolderList: string[] =
sasSessionFolder await listFilesInFolder(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,9 +183,17 @@ 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)

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

View File

@@ -3,12 +3,11 @@ 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>(function Alert( const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
props, function Alert(props, ref) {
ref return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
) { }
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

@@ -7,8 +7,10 @@
border: none; border: none;
outline: none; outline: none;
transition: 0.4s; transition: 0.4s;
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, box-shadow:
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.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;
} }
.ChunkDetails { .ChunkDetails {

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