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

Compare commits

..

43 Commits

Author SHA1 Message Date
semantic-release-bot
8b5abcd661 chore(release): 0.39.3 [skip ci]
## [0.39.3](https://github.com/sasjs/server/compare/v0.39.2...v0.39.3) (2025-11-25)

### Bug Fixes

* (deps) bump @sasjs/core to 4.59.7 ([ab96653](ab96653564))
* (deps) rerun npm i to sync ([225f381](225f381bdf))
2025-11-25 12:39:15 +00:00
Allan Bowe
48e8cb7b2d Merge pull request #385 from sasjs/bumpCore_20251125
fix: (deps) rerun npm i to sync
2025-11-25 12:36:18 +00:00
Trevor Moody
225f381bdf fix: (deps) rerun npm i to sync 2025-11-25 12:27:14 +00:00
Allan Bowe
3f49186e3b Merge pull request #384 from sasjs/bumpCore_20251125
fix: (deps) bump @sasjs/core to 4.59.7
2025-11-25 12:24:27 +00:00
Trevor Moody
ab96653564 fix: (deps) bump @sasjs/core to 4.59.7 2025-11-25 12:18:50 +00:00
semantic-release-bot
471c28eaa2 chore(release): 0.39.2 [skip ci]
## [0.39.2](https://github.com/sasjs/server/compare/v0.39.1...v0.39.2) (2025-09-25)

### Bug Fixes

* addressing test fail ([e51b204](e51b20421a))
* packages missmatch ([379ea60](379ea604bc))
* type libs ([6d123c3](6d123c3e23))
* typescript errors ([631e956](631e95604b))
* typescript errors ([198cd79](198cd79354))
2025-09-25 16:57:01 +00:00
Allan Bowe
584ffe9e0e Merge pull request #383 from sasjs/npm_update_20250919
Npm update 20250919
2025-09-25 17:53:46 +01:00
M
e51b20421a fix: addressing test fail 2025-09-25 13:49:32 +02:00
M
631e95604b fix: typescript errors 2025-09-25 13:40:10 +02:00
M
198cd79354 fix: typescript errors 2025-09-25 13:34:55 +02:00
M
379ea604bc fix: packages missmatch 2025-09-25 13:12:23 +02:00
M
9ffa403bcb chore: package-lock 2025-09-25 13:06:06 +02:00
M
6d123c3e23 fix: type libs 2025-09-25 13:03:47 +02:00
M
dda1aadc67 chore(git): Merge branch 'main' into npm_update_20250919 2025-09-25 12:48:10 +02:00
M
d47cf15cdb ci: ubuntu 22 2025-09-25 12:46:19 +02:00
Trevor Moody
d0c7968d66 build: updated package dependencies for /web 2025-09-19 18:24:58 +01:00
Trevor Moody
a5c99971cc build: server/api dependency update 2025-09-19 14:06:50 +01:00
semantic-release-bot
c422e7f02e chore(release): 0.39.1 [skip ci]
## [0.39.1](https://github.com/sasjs/server/compare/v0.39.0...v0.39.1) (2025-03-13)

### Bug Fixes

* extra bit of sleep for file recognition ([f4768bf](f4768bffd3)), closes [#381](https://github.com/sasjs/server/issues/381)
2025-03-13 10:59:10 +00:00
Allan Bowe
02a993611c Merge pull request #382 from sasjs/381-add-slight-delay-to-enable-file-detection
fix: extra bit of sleep for file recognition
2025-03-13 10:56:13 +00:00
aca2fff4ac chore(workflow): run the build workflow on ubuntu 20.04 2025-03-13 15:50:23 +05:00
af1a386b13 chore(workflow): install openssl 1.1 in actions 2025-03-13 15:43:20 +05:00
Allan Bowe
f5018ce1df chore: prettier fix 2025-03-12 17:33:02 +00:00
Allan Bowe
3529232f1f chore: whitespace removal 2025-03-12 17:29:02 +00:00
Allan Bowe
f4768bffd3 fix: extra bit of sleep for file recognition
closes #381
2025-03-12 17:27:57 +00:00
semantic-release-bot
c261745f1d chore(release): 0.39.0 [skip ci]
# [0.39.0](https://github.com/sasjs/server/compare/v0.38.0...v0.39.0) (2024-10-31)

### Bug Fixes

* **api:** fixed condition in processProgram ([48a9a4d](48a9a4dd0e))

### Features

* **api:** added session state endpoint ([6b6546c](6b6546c7ad))
2024-10-31 12:54:02 +00:00
Yury Shkoda
d6e527ecf2 Merge pull request #379 from sasjs/issue-378
Issue 378
2024-10-31 15:51:13 +03:00
Yury
bc2cff1d0d chore(api): updated trigger endpoints description 2024-10-31 15:30:32 +03:00
Yury
66aa9b5891 chore(api): updated trigger endpoints description 2024-10-31 15:20:35 +03:00
Yury
ca17e7c192 chore(api): updated endpoint description 2024-10-31 14:08:56 +03:00
Yury
73df102422 chore(api): updated endpoint description 2024-10-31 12:27:56 +03:00
Yury
48a9a4dd0e fix(api): fixed condition in processProgram 2024-10-31 11:17:20 +03:00
Yury
4f6f735f5b chore(lint): fixed code style issue 2024-10-31 10:08:34 +03:00
Yury
6b6546c7ad feat(api): added session state endpoint 2024-10-30 17:42:50 +03:00
Yury
f94ddc0352 refactor(session): implemented session state 2024-10-30 15:33:06 +03:00
Yury
03670cf0d6 chore(swagger): fixed code/stp trigger examples 2024-10-30 15:25:03 +03:00
semantic-release-bot
ea2ec97c1c chore(release): 0.38.0 [skip ci]
# [0.38.0](https://github.com/sasjs/server/compare/v0.37.0...v0.38.0) (2024-10-30)

### Features

* **api:** enabled query params in stp/trigger endpoint ([5cda9cd](5cda9cd5d8))
2024-10-30 09:25:17 +00:00
Yury Shkoda
832f1156e8 Merge pull request #377 from sasjs/issue-373-stp-fix
feat(api): enabled query params in stp/trigger endpoint
2024-10-30 12:22:10 +03:00
Yury
5cda9cd5d8 feat(api): enabled query params in stp/trigger endpoint 2024-10-30 09:39:47 +03:00
semantic-release-bot
5d576aff91 chore(release): 0.37.0 [skip ci]
# [0.37.0](https://github.com/sasjs/server/compare/v0.36.0...v0.37.0) (2024-10-29)

### Features

* **stp:** added trigger endpoint ([b0723f1](b0723f1444))
2024-10-29 14:11:35 +00:00
Yury Shkoda
a044176054 Merge pull request #375 from sasjs/issue-373-stp
Issue 373 stp
2024-10-29 17:08:38 +03:00
Yury
deee34f5fd chore(stp): removed query logic from trigger endpoint 2024-10-29 16:55:40 +03:00
Yury
b0723f1444 feat(stp): added trigger endpoint 2024-10-29 16:27:53 +03:00
Yury
e9519cb3c6 chore(code): used correct type 2024-10-29 16:20:27 +03:00
70 changed files with 12298 additions and 24232 deletions

View File

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

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:

View File

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

View File

@@ -1,3 +1,55 @@
## [0.39.3](https://github.com/sasjs/server/compare/v0.39.2...v0.39.3) (2025-11-25)
### Bug Fixes
* (deps) bump @sasjs/core to 4.59.7 ([ab96653](https://github.com/sasjs/server/commit/ab966535642d08d4e8e984007b98c8fdffbe30f7))
* (deps) rerun npm i to sync ([225f381](https://github.com/sasjs/server/commit/225f381bdf8ad5aa2af8d75648df1dd5175e12e0))
## [0.39.2](https://github.com/sasjs/server/compare/v0.39.1...v0.39.2) (2025-09-25)
### Bug Fixes
* addressing test fail ([e51b204](https://github.com/sasjs/server/commit/e51b20421adc1598ea267c79b1fb4dbc085f97b9))
* packages missmatch ([379ea60](https://github.com/sasjs/server/commit/379ea604bcb5686b5299fae6a32f759c45b275ea))
* type libs ([6d123c3](https://github.com/sasjs/server/commit/6d123c3e23628c1d703eaa13142c77f0da970a55))
* typescript errors ([631e956](https://github.com/sasjs/server/commit/631e95604b64b1a96f2abade659348618f3b00b2))
* typescript errors ([198cd79](https://github.com/sasjs/server/commit/198cd79354254511c21ac1acfbf7b6bcfdab2af7))
## [0.39.1](https://github.com/sasjs/server/compare/v0.39.0...v0.39.1) (2025-03-13)
### Bug Fixes
* extra bit of sleep for file recognition ([f4768bf](https://github.com/sasjs/server/commit/f4768bffd3dbb2fe243966572ba74002024d96e1)), closes [#381](https://github.com/sasjs/server/issues/381)
# [0.39.0](https://github.com/sasjs/server/compare/v0.38.0...v0.39.0) (2024-10-31)
### Bug Fixes
* **api:** fixed condition in processProgram ([48a9a4d](https://github.com/sasjs/server/commit/48a9a4dd0e31f84209635382be4ec4bb2c3a9c0c))
### Features
* **api:** added session state endpoint ([6b6546c](https://github.com/sasjs/server/commit/6b6546c7ad0833347f8dc4cdba6ad19132f7aaef))
# [0.38.0](https://github.com/sasjs/server/compare/v0.37.0...v0.38.0) (2024-10-30)
### Features
* **api:** enabled query params in stp/trigger endpoint ([5cda9cd](https://github.com/sasjs/server/commit/5cda9cd5d8623b7ea2ecd989d7808f47ec866672))
# [0.37.0](https://github.com/sasjs/server/compare/v0.36.0...v0.37.0) (2024-10-29)
### Features
* **stp:** added trigger endpoint ([b0723f1](https://github.com/sasjs/server/commit/b0723f14448d60ffce4f2175cf8a73fc4d4dd0ee))
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)

20474
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -48,25 +48,25 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.40.1",
"@sasjs/utils": "3.2.0",
"@sasjs/core": "^4.59.7",
"@sasjs/utils": "^3.5.2",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-session": "^1.17.2",
"express": "^4.21.2",
"express-session": "^1.18.2",
"helmet": "^5.0.2",
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.2",
"ldapjs": "2.3.3",
"mongoose": "^6.0.12",
"morgan": "^1.10.0",
"mongoose": "^6.13.8",
"morgan": "^1.10.1",
"multer": "^1.4.5-lts.1",
"rate-limiter-flexible": "2.4.1",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"unzipper": "^0.12.3",
"url": "^0.10.3"
},
"devDependencies": {
@@ -76,32 +76,32 @@
"@types/cors": "^2.8.12",
"@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.5",
"@types/ldapjs": "^2.2.4",
"@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
"@types/node": "^20.0.0",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"axios": "0.27.2",
"axios": "^1.12.2",
"csrf": "^3.1.0",
"dotenv": "^16.0.1",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"jest": "^29.7.0",
"mongodb-memory-server": "8.11.4",
"nodejs-file-downloader": "4.10.2",
"nodemon": "^2.0.7",
"nodemon": "^3.0.0",
"pkg": "5.6.0",
"prettier": "^2.3.1",
"prettier": "^3.0.0",
"rimraf": "^3.0.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-jest": "^29.1.0",
"ts-node": "^10.0.0",
"tsoa": "3.14.1",
"typescript": "^4.3.2"
"typescript": "^5.0.0"
},
"nodemonConfig": {
"ignore": [

View File

@@ -40,7 +40,8 @@ components:
clientId:
type: string
userId:
type: string
type: number
format: double
required:
- clientId
- userId
@@ -112,8 +113,8 @@ components:
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'' }'
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store code outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
example: 20241028074744-54132-1730101664824
required:
- sessionId
type: object
@@ -314,8 +315,9 @@ components:
additionalProperties: false
UserResponse:
properties:
uid:
type: string
id:
type: number
format: double
username:
type: string
displayName:
@@ -323,7 +325,7 @@ components:
isAdmin:
type: boolean
required:
- uid
- id
- username
- displayName
- isAdmin
@@ -331,30 +333,32 @@ components:
additionalProperties: false
GroupResponse:
properties:
uid:
type: string
groupId:
type: number
format: double
name:
type: string
description:
type: string
required:
- uid
- groupId
- name
- description
type: object
additionalProperties: false
UserDetailsResponse:
properties:
uid:
id:
type: number
format: double
displayName:
type: string
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
isActive:
type: boolean
isAdmin:
type: boolean
autoExec:
type: string
groups:
@@ -362,11 +366,11 @@ components:
$ref: '#/components/schemas/GroupResponse'
type: array
required:
- uid
- username
- id
- displayName
- isAdmin
- username
- isActive
- isAdmin
type: object
additionalProperties: false
UserPayload:
@@ -402,8 +406,9 @@ components:
additionalProperties: false
GroupDetailsResponse:
properties:
uid:
type: string
groupId:
type: number
format: double
name:
type: string
description:
@@ -415,7 +420,7 @@ components:
$ref: '#/components/schemas/UserResponse'
type: array
required:
- uid
- groupId
- name
- description
- isActive
@@ -484,8 +489,9 @@ components:
additionalProperties: false
PermissionDetailsResponse:
properties:
uid:
type: string
permissionId:
type: number
format: double
path:
type: string
type:
@@ -497,7 +503,7 @@ components:
group:
$ref: '#/components/schemas/GroupDetailsResponse'
required:
- uid
- permissionId
- path
- type
- setting
@@ -536,8 +542,10 @@ components:
description: 'Indicates the type of principal'
example: user
principalId:
type: string
type: number
format: double
description: 'The id of user or group to which a rule is assigned.'
example: 123
required:
- path
- type
@@ -556,39 +564,35 @@ components:
- setting
type: object
additionalProperties: false
Pick_UserResponse.Exclude_keyofUserResponse.uid__:
properties:
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
required:
- username
- displayName
- isAdmin
type: object
description: 'From T, pick a set of properties whose keys are in the union K'
SessionResponse:
properties:
id:
type: number
format: double
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
id:
type: string
needsToUpdatePassword:
type: boolean
required:
- id
- username
- displayName
- isAdmin
- id
- needsToUpdatePassword
type: object
additionalProperties: false
SessionState:
enum:
- initialising
- pending
- running
- completed
- failed
type: string
ExecutePostRequestPayload:
properties:
_program:
@@ -597,6 +601,16 @@ components:
example: /Public/somefolder/some.file
type: object
additionalProperties: false
TriggerProgramResponse:
properties:
sessionId:
type: string
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store program outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
example: 20241028074744-54132-1730101664824
required:
- sessionId
type: object
additionalProperties: false
LoginPayload:
properties:
username:
@@ -1264,7 +1278,7 @@ paths:
type: array
examples:
'Example 1':
value: [{uid: userIdString, username: johnusername, displayName: John, isAdmin: false}, {uid: anotherUserIdString, username: starkusername, displayName: Stark, isAdmin: true}]
value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}]
summary: 'Get list of all users (username, displayname). All users can request this.'
tags:
- User
@@ -1283,7 +1297,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'Example 1':
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.'
tags:
- User
@@ -1334,7 +1348,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'Example 1':
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags:
- User
@@ -1385,7 +1399,7 @@ paths:
password:
type: string
type: object
'/SASjsApi/user/{uid}':
'/SASjsApi/user/{userId}':
get:
operationId: GetUser
responses:
@@ -1404,12 +1418,14 @@ paths:
bearerAuth: []
parameters:
-
description: 'The user''s identifier'
in: path
name: uid
name: userId
required: true
schema:
type: string
'/SASjsApi/user/{userId}':
format: double
type: number
example: 1234
patch:
operationId: UpdateUser
responses:
@@ -1421,7 +1437,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'Example 1':
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags:
- User
@@ -1435,7 +1451,8 @@ paths:
name: userId
required: true
schema:
type: string
format: double
type: number
example: '1234'
requestBody:
required: true
@@ -1461,7 +1478,8 @@ paths:
name: userId
required: true
schema:
type: string
format: double
type: number
example: 1234
requestBody:
required: true
@@ -1486,7 +1504,7 @@ paths:
type: array
examples:
'Example 1':
value: [{uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users'}]
value: [{groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users'}]
summary: 'Get list of all groups (groupName and groupDescription). All users can request this.'
tags:
- Group
@@ -1505,7 +1523,7 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'Example 1':
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Create a new group. Admin only.'
tags:
- Group
@@ -1521,7 +1539,7 @@ paths:
$ref: '#/components/schemas/GroupPayload'
'/SASjsApi/group/by/groupname/{name}':
get:
operationId: GetGroupByName
operationId: GetGroupByGroupName
responses:
'200':
description: Ok
@@ -1543,7 +1561,7 @@ paths:
required: true
schema:
type: string
'/SASjsApi/group/{uid}':
'/SASjsApi/group/{groupId}':
get:
operationId: GetGroup
responses:
@@ -1563,11 +1581,12 @@ paths:
-
description: 'The group''s identifier'
in: path
name: uid
name: groupId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: 1234
delete:
operationId: DeleteGroup
responses:
@@ -1589,12 +1608,13 @@ paths:
-
description: 'The group''s identifier'
in: path
name: uid
name: groupId
required: true
schema:
type: string
example: 12ByteString
'/SASjsApi/group/{groupUid}/{userUid}':
format: double
type: number
example: 1234
'/SASjsApi/group/{groupId}/{userId}':
post:
operationId: AddUserToGroup
responses:
@@ -1606,7 +1626,7 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'Example 1':
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Add a user to a group. Admin task only.'
tags:
- Group
@@ -1617,18 +1637,21 @@ paths:
-
description: 'The group''s identifier'
in: path
name: groupUid
name: groupId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: '1234'
-
description: 'The user''s identifier'
in: path
name: userUid
name: userId
required: true
schema:
type: string
format: double
type: number
example: '6789'
delete:
operationId: RemoveUserFromGroup
responses:
@@ -1640,8 +1663,8 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'Example 1':
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Remove a user from a group. Admin task only.'
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Remove a user to a group. Admin task only.'
tags:
- Group
security:
@@ -1651,19 +1674,21 @@ paths:
-
description: 'The group''s identifier'
in: path
name: groupUid
name: groupId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: '1234'
-
description: 'The user''s identifier'
in: path
name: userUid
name: userId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: '6789'
/SASjsApi/info:
get:
operationId: Info
@@ -1714,7 +1739,7 @@ paths:
type: array
examples:
'Example 1':
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: []}}]
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: []}}]
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.'
tags:
@@ -1734,7 +1759,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse'
examples:
'Example 1':
value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
summary: 'Create a new permission. Admin only.'
tags:
- Permission
@@ -1748,7 +1773,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/RegisterPermissionPayload'
'/SASjsApi/permission/{uid}':
'/SASjsApi/permission/{permissionId}':
patch:
operationId: UpdatePermission
responses:
@@ -1760,7 +1785,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse'
examples:
'Example 1':
value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
summary: 'Update permission setting. Admin only'
tags:
- Permission
@@ -1769,11 +1794,14 @@ paths:
bearerAuth: []
parameters:
-
description: 'The permission''s identifier'
in: path
name: uid
name: permissionId
required: true
schema:
type: string
format: double
type: number
example: 1234
requestBody:
required: true
content:
@@ -1793,11 +1821,14 @@ paths:
bearerAuth: []
parameters:
-
description: 'The user''s identifier'
in: path
name: uid
name: permissionId
required: true
schema:
type: string
format: double
type: number
example: 1234
/SASjsApi/session:
get:
operationId: Session
@@ -1810,7 +1841,7 @@ paths:
$ref: '#/components/schemas/SessionResponse'
examples:
'Example 1':
value: {id: userIdString, username: johnusername, displayName: John, isAdmin: false, needsToUpdatePassword: false}
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
summary: 'Get session info (username).'
tags:
- Session
@@ -1818,6 +1849,30 @@ paths:
-
bearerAuth: []
parameters: []
'/SASjsApi/session/{sessionId}/state':
get:
operationId: SessionState
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/SessionState'
description: "The polling endpoint is currently implemented for single-server deployments only.<br>\nLoad balanced / grid topologies will be supported in a future release.<br>\nIf your site requires this, please reach out to SASjs Support."
summary: 'Get session state (initialising, pending, running, completed, failed).'
tags:
- Session
security:
-
bearerAuth: []
parameters:
-
in: path
name: sessionId
required: true
schema:
type: string
/SASjsApi/stp/execute:
get:
operationId: ExecuteGetRequest
@@ -1888,6 +1943,50 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExecutePostRequestPayload'
/SASjsApi/stp/trigger:
post:
operationId: TriggerProgram
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerProgramResponse'
description: 'Trigger Program on the Specified Runtime.'
summary: 'Triggers program and returns SessionId immediately - does not wait for program completion.'
tags:
- STP
security:
-
bearerAuth: []
parameters:
-
description: 'Location of code in SASjs Drive.'
in: query
name: _program
required: true
schema:
type: string
example: /Projects/myApp/some/program
-
description: 'Optional query param for setting debug mode.'
in: query
name: _debug
required: false
schema:
format: double
type: number
example: 131
-
description: 'Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.'
in: query
name: expiresAfterMins
required: false
schema:
format: double
type: number
example: 15
/:
get:
operationId: Home
@@ -1913,7 +2012,7 @@ paths:
application/json:
schema:
properties:
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
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}
loggedIn: {type: boolean}
required:
- user

View File

@@ -27,14 +27,14 @@ import User from '../model/User'
@Tags('Auth')
export class AuthController {
static authCodes: { [key: string]: { [key: string]: string } } = {}
static saveCode = (userId: string, clientId: string, code: string) => {
static saveCode = (userId: number, clientId: string, code: string) => {
if (AuthController.authCodes[userId])
return (AuthController.authCodes[userId][clientId] = code)
AuthController.authCodes[userId] = { [clientId]: code }
return AuthController.authCodes[userId][clientId]
}
static deleteCode = (userId: string, clientId: string) =>
static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId]
/**
@@ -159,7 +159,7 @@ const updatePassword = async (
) => {
const { currentPassword, newPassword } = data
const userId = req.user?.userId
const dbUser = await User.findOne({ _id: userId })
const dbUser = await User.findOne({ id: userId })
if (!dbUser)
throw {
@@ -234,9 +234,10 @@ const verifyAuthCode = async (
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
if (err) return resolve(undefined)
const payload = data as InfoJWT
const clientInfo: InfoJWT = {
clientId: data?.clientId,
userId: data?.userId
clientId: payload?.clientId,
userId: payload?.userId
}
if (clientInfo.clientId === clientId) {
return resolve(clientInfo)

View File

@@ -42,10 +42,12 @@ interface TriggerCodePayload {
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` is the ID of the session and the name of the temporary folder
* used to store code outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@@ -120,7 +122,7 @@ const executeCode = async (
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<{ sessionId: string }> => {
): Promise<TriggerCodeResponse> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server

View File

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

View File

@@ -2,7 +2,7 @@ import path from 'path'
import fs from 'fs'
import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types'
import { PreProgramVars, Session, TreeNode, SessionState } from '../../types'
import {
extractHeaders,
getFilesFolder,
@@ -75,8 +75,7 @@ export class ExecutionController {
const session =
sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true
session.consumed = true
session.state = SessionState.running
const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt')
@@ -121,7 +120,7 @@ export class ExecutionController {
: ''
// it should be deleted by scheduleSessionDestroy
session.inUse = false
session.state = SessionState.completed
const resultParts = []
@@ -145,7 +144,9 @@ export class ExecutionController {
return {
httpHeaders,
result:
isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout
isDebugOn(vars) || session.failureReason
? resultParts.join(`\n`)
: webout
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -106,7 +106,10 @@ const login = async (
const rateLimiter = RateLimiter.getInstance()
if (!validPass) {
const retrySecs = await rateLimiter.consume(req.ip, user?.username)
const retrySecs = await rateLimiter.consume(
req.ip || 'unknown',
user?.username
)
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
}
@@ -114,7 +117,7 @@ const login = async (
if (!validPass) throw errors.invalidPassword
// Reset on successful authorization
rateLimiter.resetOnSuccess(req.ip, user.username)
rateLimiter.resetOnSuccess(req.ip || 'unknown', user.username)
req.session.loggedIn = true
req.session.user = {

View File

@@ -37,10 +37,10 @@ export const authenticateAccessToken: RequestHandler = async (
if (user.isActive) {
req.user = user
return csrfProtection(req, res, nextFunction)
} else return res.sendStatus(401)
} else return res.status(401).send('Unauthorized')
}
}
return res.sendStatus(401)
return res.status(401).send('Unauthorized')
}
await authenticateToken(
@@ -76,7 +76,7 @@ const authenticateToken = async (
const { MODE } = process.env
if (MODE === ModeType.Desktop) {
req.user = {
userId: '1234',
userId: 1234,
clientId: 'desktopModeClientId',
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',
@@ -118,6 +118,6 @@ const authenticateToken = async (
return next()
}
res.sendStatus(401)
res.status(401).send('Unauthorized')
}
}

View File

@@ -18,7 +18,7 @@ export const authorize: RequestHandler = async (req, res, next) => {
// no need to check for permissions when route is Public
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)
const path = getPath(req)

View File

@@ -3,7 +3,7 @@ import { convertSecondsToHms } from '@sasjs/utils'
import { RateLimiter } from '../utils'
export const bruteForceProtection: RequestHandler = async (req, res, next) => {
const ip = req.ip
const ip = req.ip || 'unknown'
const username = req.body.username
const rateLimiter = RateLimiter.getInstance()

View File

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

View File

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

15
api/src/model/Counter.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -145,7 +145,7 @@ describe('web', () => {
expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({
id: expect.any(String),
id: expect.any(Number),
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin,
@@ -277,7 +277,10 @@ const performLogin = async (
.set('x-xsrf-token', csrfToken)
.send(credentials)
return { authCookies: header['set-cookie'].join() }
return {
authCookies:
(header['set-cookie'] as unknown as string[] | undefined)?.join() || ''
}
}
const extractCSRF = (text: string) =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
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.
return {
username: user ? user.username : 'demo',
userId: user ? user.userId : 'demoId',
userId: user ? user.userId : 0,
displayName: user ? user.displayName : 'demo',
serverUrl: protocol + host,
httpHeaders

View File

@@ -0,0 +1,15 @@
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

@@ -1,17 +1,19 @@
import jwt from 'jsonwebtoken'
import User from '../model/User'
import { InfoJWT } from '../types/InfoJWT'
const isValidToken = async (
token: string,
key: string,
userId: string,
userId: number,
clientId: string
) => {
const promise = new Promise<boolean>((resolve, reject) =>
jwt.verify(token, key, (err, decoded) => {
if (err) return reject(false)
if (decoded?.userId === userId && decoded?.clientId === clientId) {
const payload = decoded as InfoJWT
if (payload?.userId === userId && payload?.clientId === clientId) {
return resolve(true)
}
@@ -22,8 +24,8 @@ const isValidToken = async (
return await promise.then(() => true).catch(() => false)
}
export const getTokensFromDB = async (userId: string, clientId: string) => {
const user = await User.findOne({ _id: userId })
export const getTokensFromDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ id: userId })
if (!user) return
const currentTokenObj = user.tokens.find(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,11 +12,6 @@ const groupnameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
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 =>
Joi.object({
username: usernameSchema.required()
@@ -118,7 +113,7 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
principalType: Joi.string()
.required()
.valid(...Object.values(PrincipalType)),
principalId: Joi.string().length(24).hex().required()
principalId: Joi.number().required()
}).validate(data)
export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
@@ -197,3 +192,17 @@ export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
})
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
.validate(data)
export const triggerProgramValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_program: Joi.string().required(),
_debug: Joi.number(),
expiresAfterMins: Joi.number().greater(0)
})
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
.validate(data)
export const sessionIdValidation = (data: any): Joi.ValidationResult =>
Joi.object({
sessionId: Joi.string().required()
}).validate(data)

View File

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

12864
package-lock.json generated

File diff suppressed because it is too large Load Diff

1263
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,8 @@
"@types/jest": "^26.0.24",
"@types/node": "^12.20.28",
"@types/react": "^17.0.27",
"axios": "^0.24.0",
"axios": "^1.12.2",
"monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
@@ -54,6 +53,7 @@
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "5.5.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"path": "0.12.7",
"prettier": "^2.4.1",
"sass": "^1.44.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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