mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
86 Commits
v0.33.0
...
6690cafbf7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6690cafbf7 | ||
|
|
0838b8112e | ||
|
|
441f8b7726 | ||
|
|
049a7f4b80 | ||
|
|
3053c68bdf | ||
|
|
76750e864d | ||
|
|
ffcf193b87 | ||
|
|
aa2a1cbe13 | ||
|
|
6f2c53555c | ||
|
|
73d965daf5 | ||
|
|
4f1763db67 | ||
|
|
28222add04 | ||
|
|
068edfd6a5 | ||
|
|
7e8cbbf377 | ||
| c43afabe28 | |||
| 1531e9cd9c | |||
|
|
1fc1431442 | ||
|
|
3387efbb9a | ||
|
|
e2996b495f | ||
|
|
41c627f93a | ||
|
|
49f5dc7555 | ||
|
|
f6e77f99a4 | ||
|
|
b57dfa429b | ||
| 9586dbb2d0 | |||
|
|
a4f78ab48d | ||
|
|
2f47a2213b | ||
|
|
0f91395fbb | ||
|
|
167b14fed0 | ||
|
|
8940f4dc47 | ||
| 8cdf605006 | |||
| 3f815e9beb | |||
| 6c88eeabd2 | |||
| 093fe90589 | |||
|
|
48c1ada1b6 | ||
|
|
0532488b55 | ||
|
|
d458b5bb81 | ||
|
|
958ab9cad2 | ||
|
|
78ceed13e1 | ||
|
|
a17814fc90 | ||
|
|
9aaffce820 | ||
|
|
e78f87f5c0 | ||
|
|
bd1b58086d | ||
|
|
9f521634d9 | ||
|
|
a696168443 | ||
|
|
31df72ad88 | ||
|
|
d2239f75c2 | ||
|
|
45428892cc | ||
| ac27a9b894 | |||
| dba53de646 | |||
|
|
eb42683fff | ||
|
|
d2de9dc13e | ||
|
|
6dd2f4f876 | ||
|
|
c0f38ba7c9 | ||
|
|
d2f011e8a9 | ||
|
|
5215633e96 | ||
|
|
64b156f762 | ||
|
|
9c5acd6de3 | ||
|
|
3e72384a63 | ||
|
|
df5d40b445 | ||
|
|
c44ec35b3d | ||
|
|
77fac663c5 | ||
|
|
3848bb0add | ||
|
|
56a522c07c | ||
|
|
87e9172cfc | ||
| 7df9588e66 | |||
| 6a520f5b26 | |||
|
|
777b3a55be | ||
|
|
70c3834022 | ||
|
|
dbf6c7de08 | ||
|
|
d49ea47bd7 | ||
|
|
a38a9f9c3d | ||
|
|
be4951d112 | ||
|
|
c116b263d9 | ||
|
|
b4436bad0d | ||
|
|
57b7f954a1 | ||
|
|
8254b78955 | ||
|
|
75f5a3c0b3 | ||
|
|
c72ecc7e59 | ||
|
|
e04300ad2a | ||
|
|
c7a73991a7 | ||
|
|
02e2b060f9 | ||
|
|
3b1e4a128b | ||
|
|
7b12591595 | ||
|
|
3a887dec55 | ||
|
|
7c1c1e2410 | ||
|
|
15774eca34 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -56,4 +56,4 @@ jobs:
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release
|
||||
|
||||
112
CHANGELOG.md
112
CHANGELOG.md
@@ -1,3 +1,115 @@
|
||||
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **code:** added code/trigger API endpoint ([ffcf193](https://github.com/sasjs/server/commit/ffcf193b87d811b166d79af74013776a253b50b0))
|
||||
|
||||
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** fixed env issue in MacOS executable ([73d965d](https://github.com/sasjs/server/commit/73d965daf54b16c0921e4b18d11a1e6f8650884d))
|
||||
|
||||
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable embedded LFs in JS STP vars ([7e8cbbf](https://github.com/sasjs/server/commit/7e8cbbf377b27a7f5dd9af0bc6605c01f302f5d9))
|
||||
|
||||
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](https://github.com/sasjs/server/commit/9586dbb2d0d6611061c9efdfb84030144f62c2ee))
|
||||
|
||||
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **log-separator:** log separator should always wrap log ([8940f4d](https://github.com/sasjs/server/commit/8940f4dc47abae2036b4fcdeb772c31a0ca07cca))
|
||||
|
||||
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** fixed log/webout/print tabs ([d2de9dc](https://github.com/sasjs/server/commit/d2de9dc13ef2e980286dd03cca5e22cea443ed0c))
|
||||
* **execute:** added atribute indicating stp api ([e78f87f](https://github.com/sasjs/server/commit/e78f87f5c00038ea11261dffb525ac8f1024e40b))
|
||||
* **execute:** fixed adding print output ([9aaffce](https://github.com/sasjs/server/commit/9aaffce82051d81bf39adb69942bb321e9795141))
|
||||
* **execution:** removed empty webout from response ([6dd2f4f](https://github.com/sasjs/server/commit/6dd2f4f87673336135bc7a6de0d2e143e192c025))
|
||||
* **webout:** fixed adding empty webout to response payload ([31df72a](https://github.com/sasjs/server/commit/31df72ad88fe2c771d0ef8445d6db9dd147c40c9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor:** parse print output in response payload ([eb42683](https://github.com/sasjs/server/commit/eb42683fff701bd5b4d2b68760fe0c3ecad573dd))
|
||||
|
||||
## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use custom logic for handling sequence ids ([dba53de](https://github.com/sasjs/server/commit/dba53de64664c9d8a40fe69de6281c53d1c73641))
|
||||
|
||||
## [0.34.1](https://github.com/sasjs/server/compare/v0.34.0...v0.34.1) (2023-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **css:** fixed css loading ([9c5acd6](https://github.com/sasjs/server/commit/9c5acd6de32afdbc186f79ae5b35375dda2e49b0))
|
||||
* **log:** fixed chunk collapsing ([64b156f](https://github.com/sasjs/server/commit/64b156f7627969b7f13022726f984fbbfe1a33ef))
|
||||
|
||||
# [0.34.0](https://github.com/sasjs/server/compare/v0.33.3...v0.34.0) (2023-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **log:** fixed checks for errors and warnings ([02e2b06](https://github.com/sasjs/server/commit/02e2b060f9bedf4806f45f5205fd87bfa2ecae90))
|
||||
* **log:** fixed default runtime ([e04300a](https://github.com/sasjs/server/commit/e04300ad2ac237be7b28a6332fa87a3bcf761c7b))
|
||||
* **log:** fixed parsing log for different runtime ([3b1e4a1](https://github.com/sasjs/server/commit/3b1e4a128b1f22ff6f3069f5aaada6bfb1b40d12))
|
||||
* **log:** fixed scrolling issue ([56a522c](https://github.com/sasjs/server/commit/56a522c07c6f6d4c26c6d3b7cd6e9ef7007067a9))
|
||||
* **log:** fixed single chunk display ([8254b78](https://github.com/sasjs/server/commit/8254b789555cb8bbb169f52b754b4ce24e876dd2))
|
||||
* **log:** fixed single chunk scrolling ([57b7f95](https://github.com/sasjs/server/commit/57b7f954a17936f39aa9b757998b5b25e9442601))
|
||||
* **log:** fixed switching runtime ([c7a7399](https://github.com/sasjs/server/commit/c7a73991a7aa25d0c75d0c00e712bdc78769300b))
|
||||
* **log:** fixing switching from SAS to other runtime ([c72ecc7](https://github.com/sasjs/server/commit/c72ecc7e5943af9536ee31cfa85398e016d5354f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **log:** added download chunk and entire log ([a38a9f9](https://github.com/sasjs/server/commit/a38a9f9c3dfe36bd55d32024c166147318216995))
|
||||
* **log:** added logComponent and LogTabWithIcons ([3a887de](https://github.com/sasjs/server/commit/3a887dec55371b6a00b92291bb681e4cccb770c0))
|
||||
* **log:** added parseErrorsAndWarnings utility ([7c1c1e2](https://github.com/sasjs/server/commit/7c1c1e241002313c10f94dd61702584b9f148010))
|
||||
* **log:** added time to downloaded log name ([3848bb0](https://github.com/sasjs/server/commit/3848bb0added69ca81a5c9419ea414bdd1c294bb))
|
||||
* **log:** put download log icon into log tab ([777b3a5](https://github.com/sasjs/server/commit/777b3a55be1ecf5b05bf755ce8b14735496509e1))
|
||||
* **log:** split large log into chunks ([75f5a3c](https://github.com/sasjs/server/commit/75f5a3c0b39665bef8b83dc7e1e8b3e5f23fc303))
|
||||
* **log:** use improved log for SAS run time only ([7b12591](https://github.com/sasjs/server/commit/7b12591595cdd5144d9311ffa06a80c5dab79364))
|
||||
|
||||
## [0.33.3](https://github.com/sasjs/server/compare/v0.33.2...v0.33.3) (2023-04-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use RateLimiterMemory instead of RateLimiterMongo ([6a520f5](https://github.com/sasjs/server/commit/6a520f5b26a3e2ed6345721b30ff4e3d9bfa903d))
|
||||
|
||||
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
|
||||
|
||||
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* applying nologo only for sas.exe ([b4436ba](https://github.com/sasjs/server/commit/b4436bad0d24d5b5a402272632db1739b1018c90)), closes [#352](https://github.com/sasjs/server/issues/352)
|
||||
|
||||
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)
|
||||
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ CORS=
|
||||
WHITELIST=
|
||||
|
||||
# HELMET Cross Origin Embedder Policy
|
||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||
# options: [true|false] default: true
|
||||
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
|
||||
HELMET_COEP=
|
||||
@@ -184,7 +184,7 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
|
||||
|
||||
|
||||
# After this, access is blocked for an hour
|
||||
# Store number for 90 days since first fail
|
||||
# Store number for 24 days since first fail
|
||||
# Once a successful login is attempted, it resets
|
||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||
|
||||
#default value is 100
|
||||
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
|
||||
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
|
||||
|
||||
#default value is 10
|
||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
|
||||
|
||||
98
api/package-lock.json
generated
98
api/package-lock.json
generated
@@ -21,7 +21,6 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "2.3.3",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"rate-limiter-flexible": "2.4.1",
|
||||
@@ -43,7 +42,6 @@
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/ldapjs": "^2.2.4",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^15.12.2",
|
||||
@@ -53,7 +51,7 @@
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "8.11.4",
|
||||
@@ -3216,25 +3214,6 @@
|
||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mongoose": {
|
||||
"version": "5.11.97",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz",
|
||||
"integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==",
|
||||
"deprecated": "Mongoose publishes its own types, so you do not need to install this package.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mongoose": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mongoose-sequence": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose-sequence/-/mongoose-sequence-3.0.6.tgz",
|
||||
"integrity": "sha512-S6DD4rSlSnUI9BQvR/ACtekpylSIm0pEKayG9NqOlkUo3Q/AZLBmdi0IozSGPQ8JcB2ZSm81nLdZPhTqyOqrQg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mongoose": "^5.10.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/morgan": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
|
||||
@@ -3672,14 +3651,6 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
|
||||
@@ -4817,12 +4788,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
|
||||
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer2": {
|
||||
@@ -8125,7 +8096,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
@@ -8583,18 +8555,6 @@
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose-sequence": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose-sequence/-/mongoose-sequence-5.3.1.tgz",
|
||||
"integrity": "sha512-kQB1ctCdAQT8YdQzoHV0CpBRsO4RNVy03SOkzM6TQKBbGBs1ZgVS4UlKsuvBPaiPt9q5tKgQZvorGJ1awbHDqA==",
|
||||
"dependencies": {
|
||||
"async": "^2.5.0",
|
||||
"lodash": "^4.17.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mongoose": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -14014,24 +13974,6 @@
|
||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mongoose": {
|
||||
"version": "5.11.97",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz",
|
||||
"integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mongoose": "*"
|
||||
}
|
||||
},
|
||||
"@types/mongoose-sequence": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose-sequence/-/mongoose-sequence-3.0.6.tgz",
|
||||
"integrity": "sha512-S6DD4rSlSnUI9BQvR/ACtekpylSIm0pEKayG9NqOlkUo3Q/AZLBmdi0IozSGPQ8JcB2ZSm81nLdZPhTqyOqrQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/mongoose": "^5.10.5"
|
||||
}
|
||||
},
|
||||
"@types/morgan": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
|
||||
@@ -14409,14 +14351,6 @@
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"async-mutex": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
|
||||
@@ -15283,9 +15217,9 @@
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
|
||||
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"duplexer2": {
|
||||
@@ -17760,7 +17694,8 @@
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
@@ -18126,15 +18061,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mongoose-sequence": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose-sequence/-/mongoose-sequence-5.3.1.tgz",
|
||||
"integrity": "sha512-kQB1ctCdAQT8YdQzoHV0CpBRsO4RNVy03SOkzM6TQKBbGBs1ZgVS4UlKsuvBPaiPt9q5tKgQZvorGJ1awbHDqA==",
|
||||
"requires": {
|
||||
"async": "^2.5.0",
|
||||
"lodash": "^4.17.20"
|
||||
}
|
||||
},
|
||||
"morgan": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "2.3.3",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"rate-limiter-flexible": "2.4.1",
|
||||
@@ -80,7 +79,6 @@
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/ldapjs": "^2.2.4",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^15.12.2",
|
||||
@@ -90,7 +88,7 @@
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "8.11.4",
|
||||
|
||||
@@ -40,8 +40,7 @@ components:
|
||||
clientId:
|
||||
type: string
|
||||
userId:
|
||||
type: number
|
||||
format: double
|
||||
type: string
|
||||
required:
|
||||
- clientId
|
||||
- userId
|
||||
@@ -98,17 +97,47 @@ components:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'Code of program'
|
||||
example: '* Code HERE;'
|
||||
description: 'The code to be executed'
|
||||
example: '* Your Code HERE;'
|
||||
runTime:
|
||||
$ref: '#/components/schemas/RunTimeType'
|
||||
description: 'runtime for program'
|
||||
description: 'The runtime for the code - eg SAS, JS, PY or R'
|
||||
example: js
|
||||
required:
|
||||
- code
|
||||
- runTime
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TriggerCodeResponse:
|
||||
properties:
|
||||
sessionId:
|
||||
type: string
|
||||
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll job status.\nThis session ID should be used to poll job status."
|
||||
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
|
||||
required:
|
||||
- sessionId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TriggerCodePayload:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'The code to be executed'
|
||||
example: '* Your Code HERE;'
|
||||
runTime:
|
||||
$ref: '#/components/schemas/RunTimeType'
|
||||
description: 'The runtime for the code - eg SAS, JS, PY or R'
|
||||
example: sas
|
||||
expiresAfterMins:
|
||||
type: number
|
||||
format: double
|
||||
description: "Amount of minutes after the completion of the job when the session must be\ndestroyed."
|
||||
example: 15
|
||||
required:
|
||||
- code
|
||||
- runTime
|
||||
type: object
|
||||
additionalProperties: false
|
||||
MemberType.folder:
|
||||
enum:
|
||||
- folder
|
||||
@@ -285,9 +314,8 @@ components:
|
||||
additionalProperties: false
|
||||
UserResponse:
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
format: double
|
||||
uid:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
displayName:
|
||||
@@ -295,7 +323,7 @@ components:
|
||||
isAdmin:
|
||||
type: boolean
|
||||
required:
|
||||
- id
|
||||
- uid
|
||||
- username
|
||||
- displayName
|
||||
- isAdmin
|
||||
@@ -303,32 +331,30 @@ components:
|
||||
additionalProperties: false
|
||||
GroupResponse:
|
||||
properties:
|
||||
groupId:
|
||||
type: number
|
||||
format: double
|
||||
uid:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
required:
|
||||
- groupId
|
||||
- uid
|
||||
- name
|
||||
- description
|
||||
type: object
|
||||
additionalProperties: false
|
||||
UserDetailsResponse:
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
format: double
|
||||
displayName:
|
||||
uid:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
isActive:
|
||||
type: boolean
|
||||
displayName:
|
||||
type: string
|
||||
isAdmin:
|
||||
type: boolean
|
||||
isActive:
|
||||
type: boolean
|
||||
autoExec:
|
||||
type: string
|
||||
groups:
|
||||
@@ -336,11 +362,11 @@ components:
|
||||
$ref: '#/components/schemas/GroupResponse'
|
||||
type: array
|
||||
required:
|
||||
- id
|
||||
- displayName
|
||||
- uid
|
||||
- username
|
||||
- isActive
|
||||
- displayName
|
||||
- isAdmin
|
||||
- isActive
|
||||
type: object
|
||||
additionalProperties: false
|
||||
UserPayload:
|
||||
@@ -376,9 +402,8 @@ components:
|
||||
additionalProperties: false
|
||||
GroupDetailsResponse:
|
||||
properties:
|
||||
groupId:
|
||||
type: number
|
||||
format: double
|
||||
uid:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
@@ -390,7 +415,7 @@ components:
|
||||
$ref: '#/components/schemas/UserResponse'
|
||||
type: array
|
||||
required:
|
||||
- groupId
|
||||
- uid
|
||||
- name
|
||||
- description
|
||||
- isActive
|
||||
@@ -459,9 +484,8 @@ components:
|
||||
additionalProperties: false
|
||||
PermissionDetailsResponse:
|
||||
properties:
|
||||
permissionId:
|
||||
type: number
|
||||
format: double
|
||||
uid:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
type:
|
||||
@@ -473,7 +497,7 @@ components:
|
||||
group:
|
||||
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||
required:
|
||||
- permissionId
|
||||
- uid
|
||||
- path
|
||||
- type
|
||||
- setting
|
||||
@@ -512,10 +536,8 @@ components:
|
||||
description: 'Indicates the type of principal'
|
||||
example: user
|
||||
principalId:
|
||||
type: number
|
||||
format: double
|
||||
type: string
|
||||
description: 'The id of user or group to which a rule is assigned.'
|
||||
example: 123
|
||||
required:
|
||||
- path
|
||||
- type
|
||||
@@ -534,25 +556,37 @@ components:
|
||||
- setting
|
||||
type: object
|
||||
additionalProperties: false
|
||||
SessionResponse:
|
||||
Pick_UserResponse.Exclude_keyofUserResponse.uid__:
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
format: double
|
||||
username:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
isAdmin:
|
||||
type: boolean
|
||||
needsToUpdatePassword:
|
||||
type: boolean
|
||||
required:
|
||||
- id
|
||||
- username
|
||||
- displayName
|
||||
- isAdmin
|
||||
- needsToUpdatePassword
|
||||
type: object
|
||||
description: 'From T, pick a set of properties whose keys are in the union K'
|
||||
SessionResponse:
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
isAdmin:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
needsToUpdatePassword:
|
||||
type: boolean
|
||||
required:
|
||||
- username
|
||||
- displayName
|
||||
- isAdmin
|
||||
- id
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ExecutePostRequestPayload:
|
||||
@@ -772,7 +806,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: [{clientId: someClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}, {clientId: someOtherClientID, clientSecret: someOtherRandomCryptoString, accessTokenExpiration: 86400}]
|
||||
summary: 'Admin only task. Returns the list of all the clients *'
|
||||
summary: 'Admin only task. Returns the list of all the clients'
|
||||
tags:
|
||||
- Client
|
||||
security:
|
||||
@@ -792,7 +826,7 @@ paths:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: 'Execute Code on the Specified Runtime'
|
||||
summary: 'Run Code and Return Webout Content and Log'
|
||||
summary: "Run Code and Return Webout Content, Log and Print output\nThe order of returned parts of the payload is:\n1. Webout (if present)\n2. Logs UUID (used as separator)\n3. Log\n4. Logs UUID (used as separator)\n5. Print (if present and if the runtime is SAS)\nPlease see"
|
||||
tags:
|
||||
- Code
|
||||
security:
|
||||
@@ -805,6 +839,30 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExecuteCodePayload'
|
||||
/SASjsApi/code/trigger:
|
||||
post:
|
||||
operationId: TriggerCode
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TriggerCodeResponse'
|
||||
description: 'Trigger Code on the Specified Runtime'
|
||||
summary: 'Triggers code and returns SessionId immediately - does not wait for job completion'
|
||||
tags:
|
||||
- Code
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TriggerCodePayload'
|
||||
/SASjsApi/drive/deploy:
|
||||
post:
|
||||
operationId: Deploy
|
||||
@@ -1206,7 +1264,7 @@ paths:
|
||||
type: array
|
||||
examples:
|
||||
'Example 1':
|
||||
value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}]
|
||||
value: [{uid: userIdString, username: johnusername, displayName: John, isAdmin: false}, {uid: anotherUserIdString, username: starkusername, displayName: Stark, isAdmin: true}]
|
||||
summary: 'Get list of all users (username, displayname). All users can request this.'
|
||||
tags:
|
||||
- User
|
||||
@@ -1225,7 +1283,7 @@ paths:
|
||||
$ref: '#/components/schemas/UserDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||
summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.'
|
||||
tags:
|
||||
- User
|
||||
@@ -1276,7 +1334,7 @@ paths:
|
||||
$ref: '#/components/schemas/UserDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
|
||||
tags:
|
||||
- User
|
||||
@@ -1327,7 +1385,7 @@ paths:
|
||||
password:
|
||||
type: string
|
||||
type: object
|
||||
'/SASjsApi/user/{userId}':
|
||||
'/SASjsApi/user/{uid}':
|
||||
get:
|
||||
operationId: GetUser
|
||||
responses:
|
||||
@@ -1346,14 +1404,12 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'The user''s identifier'
|
||||
in: path
|
||||
name: userId
|
||||
name: uid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 1234
|
||||
type: string
|
||||
'/SASjsApi/user/{userId}':
|
||||
patch:
|
||||
operationId: UpdateUser
|
||||
responses:
|
||||
@@ -1365,7 +1421,7 @@ paths:
|
||||
$ref: '#/components/schemas/UserDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
|
||||
tags:
|
||||
- User
|
||||
@@ -1379,8 +1435,7 @@ paths:
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
type: string
|
||||
example: '1234'
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -1406,8 +1461,7 @@ paths:
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
type: string
|
||||
example: 1234
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -1432,7 +1486,7 @@ paths:
|
||||
type: array
|
||||
examples:
|
||||
'Example 1':
|
||||
value: [{groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users'}]
|
||||
value: [{uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users'}]
|
||||
summary: 'Get list of all groups (groupName and groupDescription). All users can request this.'
|
||||
tags:
|
||||
- Group
|
||||
@@ -1451,7 +1505,7 @@ paths:
|
||||
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
|
||||
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
|
||||
summary: 'Create a new group. Admin only.'
|
||||
tags:
|
||||
- Group
|
||||
@@ -1467,7 +1521,7 @@ paths:
|
||||
$ref: '#/components/schemas/GroupPayload'
|
||||
'/SASjsApi/group/by/groupname/{name}':
|
||||
get:
|
||||
operationId: GetGroupByGroupName
|
||||
operationId: GetGroupByName
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
@@ -1489,7 +1543,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
'/SASjsApi/group/{groupId}':
|
||||
'/SASjsApi/group/{uid}':
|
||||
get:
|
||||
operationId: GetGroup
|
||||
responses:
|
||||
@@ -1509,12 +1563,11 @@ paths:
|
||||
-
|
||||
description: 'The group''s identifier'
|
||||
in: path
|
||||
name: groupId
|
||||
name: uid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 1234
|
||||
type: string
|
||||
example: 12ByteString
|
||||
delete:
|
||||
operationId: DeleteGroup
|
||||
responses:
|
||||
@@ -1536,13 +1589,12 @@ paths:
|
||||
-
|
||||
description: 'The group''s identifier'
|
||||
in: path
|
||||
name: groupId
|
||||
name: uid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 1234
|
||||
'/SASjsApi/group/{groupId}/{userId}':
|
||||
type: string
|
||||
example: 12ByteString
|
||||
'/SASjsApi/group/{groupUid}/{userUid}':
|
||||
post:
|
||||
operationId: AddUserToGroup
|
||||
responses:
|
||||
@@ -1554,7 +1606,7 @@ paths:
|
||||
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
|
||||
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
|
||||
summary: 'Add a user to a group. Admin task only.'
|
||||
tags:
|
||||
- Group
|
||||
@@ -1565,21 +1617,18 @@ paths:
|
||||
-
|
||||
description: 'The group''s identifier'
|
||||
in: path
|
||||
name: groupId
|
||||
name: groupUid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: '1234'
|
||||
type: string
|
||||
example: 12ByteString
|
||||
-
|
||||
description: 'The user''s identifier'
|
||||
in: path
|
||||
name: userId
|
||||
name: userUid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: '6789'
|
||||
type: string
|
||||
delete:
|
||||
operationId: RemoveUserFromGroup
|
||||
responses:
|
||||
@@ -1591,8 +1640,8 @@ paths:
|
||||
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
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.'
|
||||
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.'
|
||||
tags:
|
||||
- Group
|
||||
security:
|
||||
@@ -1602,21 +1651,19 @@ paths:
|
||||
-
|
||||
description: 'The group''s identifier'
|
||||
in: path
|
||||
name: groupId
|
||||
name: groupUid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: '1234'
|
||||
type: string
|
||||
example: 12ByteString
|
||||
-
|
||||
description: 'The user''s identifier'
|
||||
in: path
|
||||
name: userId
|
||||
name: userUid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: '6789'
|
||||
type: string
|
||||
example: 12ByteString
|
||||
/SASjsApi/info:
|
||||
get:
|
||||
operationId: Info
|
||||
@@ -1667,7 +1714,7 @@ paths:
|
||||
type: array
|
||||
examples:
|
||||
'Example 1':
|
||||
value: [{permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {permissionId: 124, path: /SASjsApi/code/execute, type: Route, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}]
|
||||
value: [{uid: permissionId1String, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: user1-id, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {uid: permissionId2String, path: /SASjsApi/code/execute, type: Route, setting: Grant, group: {uid: group1-id, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}]
|
||||
description: "Get the list of permission rules applicable the authenticated user.\nIf the user is an admin, all rules are returned."
|
||||
summary: 'Get the list of permission rules. If the user is admin, all rules are returned.'
|
||||
tags:
|
||||
@@ -1687,7 +1734,7 @@ paths:
|
||||
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
|
||||
value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
|
||||
summary: 'Create a new permission. Admin only.'
|
||||
tags:
|
||||
- Permission
|
||||
@@ -1701,7 +1748,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterPermissionPayload'
|
||||
'/SASjsApi/permission/{permissionId}':
|
||||
'/SASjsApi/permission/{uid}':
|
||||
patch:
|
||||
operationId: UpdatePermission
|
||||
responses:
|
||||
@@ -1713,7 +1760,7 @@ paths:
|
||||
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
|
||||
value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
|
||||
summary: 'Update permission setting. Admin only'
|
||||
tags:
|
||||
- Permission
|
||||
@@ -1722,14 +1769,11 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'The permission''s identifier'
|
||||
in: path
|
||||
name: permissionId
|
||||
name: uid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 1234
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1749,14 +1793,11 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'The user''s identifier'
|
||||
in: path
|
||||
name: permissionId
|
||||
name: uid
|
||||
required: true
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 1234
|
||||
type: string
|
||||
/SASjsApi/session:
|
||||
get:
|
||||
operationId: Session
|
||||
@@ -1769,7 +1810,7 @@ paths:
|
||||
$ref: '#/components/schemas/SessionResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
||||
value: {id: userIdString, username: johnusername, displayName: John, isAdmin: false, needsToUpdatePassword: false}
|
||||
summary: 'Get session info (username).'
|
||||
tags:
|
||||
- Session
|
||||
@@ -1789,7 +1830,7 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts additional URL parameters (converted to session variables)\nand file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
|
||||
tags:
|
||||
- STP
|
||||
@@ -1798,13 +1839,22 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of code in SASjs Drive'
|
||||
description: 'Location of Stored Program 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 (returns the session log in the response body).'
|
||||
in: query
|
||||
name: _debug
|
||||
required: false
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 131
|
||||
post:
|
||||
operationId: ExecutePostRequest
|
||||
responses:
|
||||
@@ -1863,7 +1913,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
|
||||
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
|
||||
loggedIn: {type: boolean}
|
||||
required:
|
||||
- user
|
||||
|
||||
@@ -27,14 +27,14 @@ import User from '../model/User'
|
||||
@Tags('Auth')
|
||||
export class AuthController {
|
||||
static authCodes: { [key: string]: { [key: string]: string } } = {}
|
||||
static saveCode = (userId: number, clientId: string, code: string) => {
|
||||
static saveCode = (userId: string, clientId: string, code: string) => {
|
||||
if (AuthController.authCodes[userId])
|
||||
return (AuthController.authCodes[userId][clientId] = code)
|
||||
|
||||
AuthController.authCodes[userId] = { [clientId]: code }
|
||||
return AuthController.authCodes[userId][clientId]
|
||||
}
|
||||
static deleteCode = (userId: number, clientId: string) =>
|
||||
static deleteCode = (userId: string, 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 {
|
||||
|
||||
@@ -1,34 +1,69 @@
|
||||
import express from 'express'
|
||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||
import { ExecutionController } from './internal'
|
||||
import { ExecutionController, getSessionController } from './internal'
|
||||
import {
|
||||
getPreProgramVariables,
|
||||
getUserAutoExec,
|
||||
ModeType,
|
||||
parseLogToArray,
|
||||
RunTimeType
|
||||
} from '../utils'
|
||||
|
||||
interface ExecuteCodePayload {
|
||||
/**
|
||||
* Code of program
|
||||
* @example "* Code HERE;"
|
||||
* The code to be executed
|
||||
* @example "* Your Code HERE;"
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* runtime for program
|
||||
* The runtime for the code - eg SAS, JS, PY or R
|
||||
* @example "js"
|
||||
*/
|
||||
runTime: RunTimeType
|
||||
}
|
||||
|
||||
interface TriggerCodePayload {
|
||||
/**
|
||||
* The code to be executed
|
||||
* @example "* Your Code HERE;"
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* The runtime for the code - eg SAS, JS, PY or R
|
||||
* @example "sas"
|
||||
*/
|
||||
runTime: RunTimeType
|
||||
/**
|
||||
* Amount of minutes after the completion of the job when the session must be
|
||||
* destroyed.
|
||||
* @example 15
|
||||
*/
|
||||
expiresAfterMins?: number
|
||||
}
|
||||
|
||||
interface TriggerCodeResponse {
|
||||
/**
|
||||
* The SessionId is the name of the temporary folder used to store the outputs.
|
||||
* For SAS, this would be the SASWORK folder. Can be used to poll job status.
|
||||
* This session ID should be used to poll job status.
|
||||
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
|
||||
*/
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/code')
|
||||
@Tags('Code')
|
||||
export class CodeController {
|
||||
/**
|
||||
* Execute Code on the Specified Runtime
|
||||
* @summary Run Code and Return Webout Content and Log
|
||||
* @summary Run Code and Return Webout Content, Log and Print output
|
||||
* The order of returned parts of the payload is:
|
||||
* 1. Webout (if present)
|
||||
* 2. Logs UUID (used as separator)
|
||||
* 3. Log
|
||||
* 4. Logs UUID (used as separator)
|
||||
* 5. Print (if present and if the runtime is SAS)
|
||||
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
|
||||
*/
|
||||
@Post('/execute')
|
||||
public async executeCode(
|
||||
@@ -37,6 +72,18 @@ export class CodeController {
|
||||
): Promise<string | Buffer> {
|
||||
return executeCode(request, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger Code on the Specified Runtime
|
||||
* @summary Triggers code and returns SessionId immediately - does not wait for job completion
|
||||
*/
|
||||
@Post('/trigger')
|
||||
public async triggerCode(
|
||||
@Request() request: express.Request,
|
||||
@Body() body: TriggerCodePayload
|
||||
): Promise<TriggerCodeResponse> {
|
||||
return triggerCode(request, body)
|
||||
}
|
||||
}
|
||||
|
||||
const executeCode = async (
|
||||
@@ -55,7 +102,8 @@ const executeCode = async (
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: { ...req.query, _debug: 131 },
|
||||
otherArgs: { userAutoExec },
|
||||
runTime: runTime
|
||||
runTime: runTime,
|
||||
includePrintOutput: true
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -68,3 +116,49 @@ const executeCode = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const triggerCode = async (
|
||||
req: express.Request,
|
||||
{ code, runTime, expiresAfterMins }: TriggerCodePayload
|
||||
): Promise<{ sessionId: string }> => {
|
||||
const { user } = req
|
||||
const userAutoExec =
|
||||
process.env.MODE === ModeType.Server
|
||||
? user?.autoExec
|
||||
: await getUserAutoExec()
|
||||
|
||||
// get session controller based on runTime
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
// get session
|
||||
const session = await sessionController.getSession()
|
||||
|
||||
// add expiresAfterMins to session if provided
|
||||
if (expiresAfterMins) {
|
||||
// expiresAfterMins.used is set initially to false
|
||||
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
|
||||
}
|
||||
|
||||
try {
|
||||
// call executeProgram method of ExecutionController without awaiting
|
||||
new ExecutionController().executeProgram({
|
||||
program: code,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: { ...req.query, _debug: 131 },
|
||||
otherArgs: { userAutoExec },
|
||||
runTime: runTime,
|
||||
includePrintOutput: true,
|
||||
session // session is provided
|
||||
})
|
||||
|
||||
// return session id
|
||||
return { sessionId: session.id }
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
message: 'Job execution failed.',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,28 +12,29 @@ import {
|
||||
|
||||
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import { AuthProviderType } from '../utils'
|
||||
import { UserResponse } from './user'
|
||||
import { GetUserBy, UserResponse } from './user'
|
||||
|
||||
export interface GroupResponse {
|
||||
groupId: number
|
||||
uid: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface GroupDetailsResponse {
|
||||
groupId: number
|
||||
name: string
|
||||
description: string
|
||||
export interface GroupDetailsResponse extends GroupResponse {
|
||||
isActive: boolean
|
||||
users: UserResponse[]
|
||||
}
|
||||
|
||||
interface GetGroupBy {
|
||||
groupId?: number
|
||||
_id?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
enum GroupAction {
|
||||
AddUser = 'addUser',
|
||||
RemoveUser = 'removeUser'
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/group')
|
||||
@Tags('Group')
|
||||
@@ -44,7 +45,7 @@ export class GroupController {
|
||||
*/
|
||||
@Example<GroupResponse[]>([
|
||||
{
|
||||
groupId: 123,
|
||||
uid: 'groupIdString',
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users'
|
||||
}
|
||||
@@ -59,7 +60,7 @@ export class GroupController {
|
||||
*
|
||||
*/
|
||||
@Example<GroupDetailsResponse>({
|
||||
groupId: 123,
|
||||
uid: 'groupIdString',
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
@@ -78,7 +79,7 @@ export class GroupController {
|
||||
* @example dcgroup
|
||||
*/
|
||||
@Get('by/groupname/{name}')
|
||||
public async getGroupByGroupName(
|
||||
public async getGroupByName(
|
||||
@Path() name: string
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return getGroup({ name })
|
||||
@@ -86,68 +87,66 @@ export class GroupController {
|
||||
|
||||
/**
|
||||
* @summary Get list of members of a group (userName). All users can request this.
|
||||
* @param groupId The group's identifier
|
||||
* @example groupId 1234
|
||||
* @param uid The group's identifier
|
||||
* @example uid "12ByteString"
|
||||
*/
|
||||
@Get('{groupId}')
|
||||
public async getGroup(
|
||||
@Path() groupId: number
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return getGroup({ groupId })
|
||||
@Get('{uid}')
|
||||
public async getGroup(@Path() uid: string): Promise<GroupDetailsResponse> {
|
||||
return getGroup({ _id: uid })
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Add 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"
|
||||
* @param groupUid The group's identifier
|
||||
* @example groupUid "12ByteString"
|
||||
* @param userUid The user's identifier
|
||||
* @example userId "12ByteString"
|
||||
*/
|
||||
@Example<GroupDetailsResponse>({
|
||||
groupId: 123,
|
||||
uid: 'groupIdString',
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
users: []
|
||||
})
|
||||
@Post('{groupId}/{userId}')
|
||||
@Post('{groupUid}/{userUid}')
|
||||
public async addUserToGroup(
|
||||
@Path() groupId: number,
|
||||
@Path() userId: number
|
||||
@Path() groupUid: string,
|
||||
@Path() userUid: string
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return addUserToGroup(groupId, userId)
|
||||
return addUserToGroup(groupUid, userUid)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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"
|
||||
* @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"
|
||||
*/
|
||||
@Example<GroupDetailsResponse>({
|
||||
groupId: 123,
|
||||
uid: 'groupIdString',
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
users: []
|
||||
})
|
||||
@Delete('{groupId}/{userId}')
|
||||
@Delete('{groupUid}/{userUid}')
|
||||
public async removeUserFromGroup(
|
||||
@Path() groupId: number,
|
||||
@Path() userId: number
|
||||
@Path() groupUid: string,
|
||||
@Path() userUid: string
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return removeUserFromGroup(groupId, userId)
|
||||
return removeUserFromGroup(groupUid, userUid)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Delete a group. Admin task only.
|
||||
* @param groupId The group's identifier
|
||||
* @example groupId 1234
|
||||
* @param uid The group's identifier
|
||||
* @example uid "12ByteString"
|
||||
*/
|
||||
@Delete('{groupId}')
|
||||
public async deleteGroup(@Path() groupId: number) {
|
||||
const group = await Group.findOne({ groupId })
|
||||
@Delete('{uid}')
|
||||
public async deleteGroup(@Path() uid: string) {
|
||||
const group = await Group.findOne({ _id: uid })
|
||||
if (!group)
|
||||
throw {
|
||||
code: 404,
|
||||
@@ -160,9 +159,7 @@ export class GroupController {
|
||||
}
|
||||
|
||||
const getAllGroups = async (): Promise<GroupResponse[]> =>
|
||||
await Group.find({})
|
||||
.select({ _id: 0, groupId: 1, name: 1, description: 1 })
|
||||
.exec()
|
||||
await Group.find({}).select('uid name description').exec()
|
||||
|
||||
const createGroup = async ({
|
||||
name,
|
||||
@@ -187,7 +184,7 @@ const createGroup = async ({
|
||||
const savedGroup = await group.save()
|
||||
|
||||
return {
|
||||
groupId: savedGroup.groupId,
|
||||
uid: savedGroup.uid,
|
||||
name: savedGroup.name,
|
||||
description: savedGroup.description,
|
||||
isActive: savedGroup.isActive,
|
||||
@@ -198,11 +195,12 @@ const createGroup = async ({
|
||||
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
||||
const group = (await Group.findOne(
|
||||
findBy,
|
||||
'groupId name description isActive users -_id'
|
||||
'uid name description isActive users'
|
||||
).populate(
|
||||
'users',
|
||||
'id username displayName isAdmin -_id'
|
||||
'uid username displayName isAdmin'
|
||||
)) as unknown as GroupDetailsResponse
|
||||
|
||||
if (!group)
|
||||
throw {
|
||||
code: 404,
|
||||
@@ -211,7 +209,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: group.groupId,
|
||||
uid: group.uid,
|
||||
name: group.name,
|
||||
description: group.description,
|
||||
isActive: group.isActive,
|
||||
@@ -220,23 +218,23 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
||||
}
|
||||
|
||||
const addUserToGroup = async (
|
||||
groupId: number,
|
||||
userId: number
|
||||
groupUid: string,
|
||||
userUid: string
|
||||
): Promise<GroupDetailsResponse> =>
|
||||
updateUsersListInGroup(groupId, userId, 'addUser')
|
||||
updateUsersListInGroup(groupUid, userUid, GroupAction.AddUser)
|
||||
|
||||
const removeUserFromGroup = async (
|
||||
groupId: number,
|
||||
userId: number
|
||||
groupUid: string,
|
||||
userUid: string
|
||||
): Promise<GroupDetailsResponse> =>
|
||||
updateUsersListInGroup(groupId, userId, 'removeUser')
|
||||
updateUsersListInGroup(groupUid, userUid, GroupAction.RemoveUser)
|
||||
|
||||
const updateUsersListInGroup = async (
|
||||
groupId: number,
|
||||
userId: number,
|
||||
action: 'addUser' | 'removeUser'
|
||||
groupUid: string,
|
||||
userUid: string,
|
||||
action: GroupAction
|
||||
): Promise<GroupDetailsResponse> => {
|
||||
const group = await Group.findOne({ groupId })
|
||||
const group = await Group.findOne({ _id: groupUid })
|
||||
if (!group)
|
||||
throw {
|
||||
code: 404,
|
||||
@@ -258,7 +256,7 @@ const updateUsersListInGroup = async (
|
||||
message: `Can't add/remove user to group created by external auth provider.`
|
||||
}
|
||||
|
||||
const user = await User.findOne({ id: userId })
|
||||
const user = await User.findOne({ _id: userUid })
|
||||
if (!user)
|
||||
throw {
|
||||
code: 404,
|
||||
@@ -274,7 +272,7 @@ const updateUsersListInGroup = async (
|
||||
}
|
||||
|
||||
const updatedGroup =
|
||||
action === 'addUser'
|
||||
action === GroupAction.AddUser
|
||||
? await group.addUser(user)
|
||||
: await group.removeUser(user)
|
||||
|
||||
@@ -286,7 +284,7 @@ const updateUsersListInGroup = async (
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: updatedGroup.groupId,
|
||||
uid: updatedGroup.uid,
|
||||
name: updatedGroup.name,
|
||||
description: updatedGroup.description,
|
||||
isActive: updatedGroup.isActive,
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ExecuteFileParams {
|
||||
|
||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||
program: string
|
||||
includePrintOutput?: boolean
|
||||
}
|
||||
|
||||
export class ExecutionController {
|
||||
@@ -67,7 +68,8 @@ export class ExecutionController {
|
||||
otherArgs,
|
||||
session: sessionByFileUpload,
|
||||
runTime,
|
||||
forceStringResult
|
||||
forceStringResult,
|
||||
includePrintOutput
|
||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
@@ -78,7 +80,6 @@ export class ExecutionController {
|
||||
|
||||
const logPath = path.join(session.path, 'log.log')
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
|
||||
const weboutPath = path.join(session.path, 'webout.txt')
|
||||
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||
|
||||
@@ -122,12 +123,29 @@ export class ExecutionController {
|
||||
// it should be deleted by scheduleSessionDestroy
|
||||
session.inUse = false
|
||||
|
||||
const resultParts = []
|
||||
|
||||
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty
|
||||
if (webout && webout.length !== 0) resultParts.push(webout)
|
||||
|
||||
// INFO: log separator wraps the log from the beginning and the end
|
||||
resultParts.push(process.logsUUID)
|
||||
resultParts.push(log)
|
||||
resultParts.push(process.logsUUID)
|
||||
|
||||
if (includePrintOutput && runTime === RunTimeType.SAS) {
|
||||
const printOutputPath = path.join(session.path, 'output.lst')
|
||||
const printOutput = (await fileExists(printOutputPath))
|
||||
? await readFile(printOutputPath)
|
||||
: ''
|
||||
|
||||
if (printOutput) resultParts.push(printOutput)
|
||||
}
|
||||
|
||||
return {
|
||||
httpHeaders,
|
||||
result:
|
||||
isDebugOn(vars) || session.crashed
|
||||
? `${webout}\n${process.logsUUID}\n${log}`
|
||||
: webout
|
||||
isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
isWindows
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
@@ -134,7 +133,7 @@ ${autoExecContent}`
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
autoExecPath,
|
||||
isWindows() ? '-nologo' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nologo' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||
@@ -190,17 +189,37 @@ ${autoExecContent}`
|
||||
}
|
||||
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
setTimeout(
|
||||
async () => {
|
||||
if (session.inUse) {
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp =
|
||||
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
await this.deleteSession(session)
|
||||
}
|
||||
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
const { expiresAfterMins } = session
|
||||
|
||||
// delay session destroy if expiresAfterMins present
|
||||
if (expiresAfterMins && !expiresAfterMins.used) {
|
||||
// calculate session death time using expiresAfterMins
|
||||
const newDeathTimeStamp =
|
||||
parseInt(session.deathTimeStamp) +
|
||||
expiresAfterMins.mins * 60 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
// set expiresAfterMins to true to avoid using it again
|
||||
session.expiresAfterMins!.used = true
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
await this.deleteSession(session)
|
||||
}
|
||||
}
|
||||
},
|
||||
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const createJSProgram = async (
|
||||
) => {
|
||||
const varStatments = Object.keys(vars).reduce(
|
||||
(computed: string, key: string) =>
|
||||
`${computed}const ${key} = '${vars[key]}';\n`,
|
||||
`${computed}const ${key} = \`${vars[key]}\`;\n`,
|
||||
''
|
||||
)
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ export const createSASProgram = async (
|
||||
%mend;
|
||||
%_sasjs_server_init()
|
||||
|
||||
proc printto print="%sysfunc(getoption(log))";
|
||||
run;
|
||||
`
|
||||
|
||||
program = `
|
||||
|
||||
@@ -56,9 +56,9 @@ interface RegisterPermissionPayload {
|
||||
principalType: PrincipalType
|
||||
/**
|
||||
* The id of user or group to which a rule is assigned.
|
||||
* @example 123
|
||||
* @example 'groupIdString'
|
||||
*/
|
||||
principalId: number
|
||||
principalId: string
|
||||
}
|
||||
|
||||
interface UpdatePermissionPayload {
|
||||
@@ -70,7 +70,7 @@ interface UpdatePermissionPayload {
|
||||
}
|
||||
|
||||
export interface PermissionDetailsResponse {
|
||||
permissionId: number
|
||||
uid: string
|
||||
path: string
|
||||
type: string
|
||||
setting: string
|
||||
@@ -91,24 +91,24 @@ export class PermissionController {
|
||||
*/
|
||||
@Example<PermissionDetailsResponse[]>([
|
||||
{
|
||||
permissionId: 123,
|
||||
uid: 'permissionId1String',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
user: {
|
||||
id: 1,
|
||||
uid: 'user1-id',
|
||||
username: 'johnSnow01',
|
||||
displayName: 'John Snow',
|
||||
isAdmin: false
|
||||
}
|
||||
},
|
||||
{
|
||||
permissionId: 124,
|
||||
uid: 'permissionId2String',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
group: {
|
||||
groupId: 1,
|
||||
uid: 'group1-id',
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
@@ -128,12 +128,12 @@ export class PermissionController {
|
||||
*
|
||||
*/
|
||||
@Example<PermissionDetailsResponse>({
|
||||
permissionId: 123,
|
||||
uid: 'permissionIdString',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
user: {
|
||||
id: 1,
|
||||
uid: 'userIdString',
|
||||
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 1234
|
||||
* @example permissionId "permissionIdString"
|
||||
*/
|
||||
@Example<PermissionDetailsResponse>({
|
||||
permissionId: 123,
|
||||
uid: 'permissionIdString',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
user: {
|
||||
id: 1,
|
||||
uid: 'userIdString',
|
||||
username: 'johnSnow01',
|
||||
displayName: 'John Snow',
|
||||
isAdmin: false
|
||||
}
|
||||
})
|
||||
@Patch('{permissionId}')
|
||||
@Patch('{uid}')
|
||||
public async updatePermission(
|
||||
@Path() permissionId: number,
|
||||
@Path() uid: string,
|
||||
@Body() body: UpdatePermissionPayload
|
||||
): Promise<PermissionDetailsResponse> {
|
||||
return updatePermission(permissionId, body)
|
||||
return updatePermission(uid, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Delete a permission. Admin only.
|
||||
* @param permissionId The user's identifier
|
||||
* @example permissionId 1234
|
||||
* @example permissionId "permissionIdString"
|
||||
*/
|
||||
@Delete('{permissionId}')
|
||||
public async deletePermission(@Path() permissionId: number) {
|
||||
return deletePermission(permissionId)
|
||||
@Delete('{uid}')
|
||||
public async deletePermission(@Path() uid: string) {
|
||||
return deletePermission(uid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
id: userInDB.id,
|
||||
uid: userInDB.uid,
|
||||
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({ groupId: principalId })
|
||||
const groupInDB = await Group.findOne({ _id: principalId })
|
||||
if (!groupInDB)
|
||||
throw {
|
||||
code: 404,
|
||||
@@ -291,13 +291,13 @@ const createPermission = async ({
|
||||
permission.group = groupInDB._id
|
||||
|
||||
group = {
|
||||
groupId: groupInDB.groupId,
|
||||
uid: groupInDB.uid,
|
||||
name: groupInDB.name,
|
||||
description: groupInDB.description,
|
||||
isActive: groupInDB.isActive,
|
||||
users: groupInDB.populate({
|
||||
path: 'users',
|
||||
select: 'id username displayName isAdmin -_id',
|
||||
select: 'uid username displayName isAdmin -_id',
|
||||
options: { limit: 15 }
|
||||
}) as unknown as UserResponse[]
|
||||
}
|
||||
@@ -314,7 +314,7 @@ const createPermission = async ({
|
||||
const savedPermission = await permission.save()
|
||||
|
||||
return {
|
||||
permissionId: savedPermission.permissionId,
|
||||
uid: savedPermission.uid,
|
||||
path: savedPermission.path,
|
||||
type: savedPermission.type,
|
||||
setting: savedPermission.setting,
|
||||
@@ -324,27 +324,21 @@ const createPermission = async ({
|
||||
}
|
||||
|
||||
const updatePermission = async (
|
||||
id: number,
|
||||
uid: string,
|
||||
data: UpdatePermissionPayload
|
||||
): Promise<PermissionDetailsResponse> => {
|
||||
const { setting } = data
|
||||
|
||||
const updatedPermission = (await Permission.findOneAndUpdate(
|
||||
{ permissionId: id },
|
||||
{ _id: uid },
|
||||
{ setting },
|
||||
{ new: true }
|
||||
)
|
||||
.select({
|
||||
_id: 0,
|
||||
permissionId: 1,
|
||||
path: 1,
|
||||
type: 1,
|
||||
setting: 1
|
||||
})
|
||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||
.select('uid path type setting')
|
||||
.populate({ path: 'user', select: 'uid username displayName isAdmin' })
|
||||
.populate({
|
||||
path: 'group',
|
||||
select: 'groupId name description -_id'
|
||||
select: 'groupId name description'
|
||||
})) as unknown as PermissionDetailsResponse
|
||||
if (!updatedPermission)
|
||||
throw {
|
||||
@@ -356,13 +350,13 @@ const updatePermission = async (
|
||||
return updatedPermission
|
||||
}
|
||||
|
||||
const deletePermission = async (id: number) => {
|
||||
const permission = await Permission.findOne({ permissionId: id })
|
||||
const deletePermission = async (uid: string) => {
|
||||
const permission = await Permission.findOne({ _id: uid })
|
||||
if (!permission)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Permission not found.'
|
||||
}
|
||||
await Permission.deleteOne({ permissionId: id })
|
||||
await Permission.deleteOne({ _id: uid })
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import express from 'express'
|
||||
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
interface SessionResponse extends UserResponse {
|
||||
needsToUpdatePassword: boolean
|
||||
interface SessionResponse extends Omit<UserResponse, 'uid'> {
|
||||
id: string
|
||||
needsToUpdatePassword?: boolean
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@@ -14,11 +15,12 @@ export class SessionController {
|
||||
* @summary Get session info (username).
|
||||
*
|
||||
*/
|
||||
@Example<UserResponse>({
|
||||
id: 123,
|
||||
@Example<SessionResponse>({
|
||||
id: 'userIdString',
|
||||
username: 'johnusername',
|
||||
displayName: 'John',
|
||||
isAdmin: false
|
||||
isAdmin: false,
|
||||
needsToUpdatePassword: false
|
||||
})
|
||||
@Get('/')
|
||||
public async session(
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
||||
import { ExecutionController, ExecutionVars } from './internal'
|
||||
import {
|
||||
getPreProgramVariables,
|
||||
HTTPHeaders,
|
||||
LogLine,
|
||||
makeFilesNamesMap,
|
||||
getRunTimeAndFilePath
|
||||
} from '../utils'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
import { debug } from 'console'
|
||||
|
||||
interface ExecutePostRequestPayload {
|
||||
/**
|
||||
@@ -25,20 +24,31 @@ export class STPController {
|
||||
/**
|
||||
* Trigger a Stored Program using the _program URL parameter.
|
||||
*
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
* Accepts additional URL parameters (converted to session variables)
|
||||
* and file uploads. For more details, see docs:
|
||||
*
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||
* @param _program Location of code in SASjs Drive
|
||||
* @param _program Location of Stored Program in SASjs Drive.
|
||||
* @param _debug Optional query param for setting debug mode (returns the session log in the response body).
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
* @example _debug 131
|
||||
*/
|
||||
@Get('/execute')
|
||||
public async executeGetRequest(
|
||||
@Request() request: express.Request,
|
||||
@Query() _program: string
|
||||
@Query() _program: string,
|
||||
@Query() _debug?: number
|
||||
): Promise<string | Buffer> {
|
||||
const vars = request.query as ExecutionVars
|
||||
let vars = request.query as ExecutionVars
|
||||
if (_debug) {
|
||||
vars = {
|
||||
...vars,
|
||||
_debug
|
||||
}
|
||||
}
|
||||
|
||||
return execute(request, _program, vars)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,18 +26,14 @@ import {
|
||||
import { GroupController, GroupResponse } from './group'
|
||||
|
||||
export interface UserResponse {
|
||||
id: number
|
||||
uid: string
|
||||
username: string
|
||||
displayName: string
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export interface UserDetailsResponse {
|
||||
id: number
|
||||
displayName: string
|
||||
username: string
|
||||
export interface UserDetailsResponse extends UserResponse {
|
||||
isActive: boolean
|
||||
isAdmin: boolean
|
||||
autoExec?: string
|
||||
groups?: GroupResponse[]
|
||||
}
|
||||
@@ -52,13 +48,13 @@ export class UserController {
|
||||
*/
|
||||
@Example<UserResponse[]>([
|
||||
{
|
||||
id: 123,
|
||||
uid: 'userIdString',
|
||||
username: 'johnusername',
|
||||
displayName: 'John',
|
||||
isAdmin: false
|
||||
},
|
||||
{
|
||||
id: 456,
|
||||
uid: 'anotherUserIdString',
|
||||
username: 'starkusername',
|
||||
displayName: 'Stark',
|
||||
isAdmin: true
|
||||
@@ -74,7 +70,7 @@ export class UserController {
|
||||
*
|
||||
*/
|
||||
@Example<UserDetailsResponse>({
|
||||
id: 1234,
|
||||
uid: 'userIdString',
|
||||
displayName: 'John Snow',
|
||||
username: 'johnSnow01',
|
||||
isAdmin: false,
|
||||
@@ -111,20 +107,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 1234
|
||||
* @example userId "userIdString"
|
||||
*/
|
||||
@Get('{userId}')
|
||||
@Get('{uid}')
|
||||
public async getUser(
|
||||
@Request() req: express.Request,
|
||||
@Path() userId: number
|
||||
@Path() uid: string
|
||||
): Promise<UserDetailsResponse> {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
|
||||
|
||||
const { user } = req
|
||||
const getAutoExec = user!.isAdmin || user!.userId == userId
|
||||
return getUser({ id: userId }, getAutoExec)
|
||||
const getAutoExec = user!.isAdmin || user!.userId === uid
|
||||
return getUser({ _id: uid }, getAutoExec)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +129,7 @@ export class UserController {
|
||||
* @example username "johnSnow01"
|
||||
*/
|
||||
@Example<UserDetailsResponse>({
|
||||
id: 1234,
|
||||
uid: 'userIdString',
|
||||
displayName: 'John Snow',
|
||||
username: 'johnSnow01',
|
||||
isAdmin: false,
|
||||
@@ -158,7 +154,7 @@ export class UserController {
|
||||
* @example userId "1234"
|
||||
*/
|
||||
@Example<UserDetailsResponse>({
|
||||
id: 1234,
|
||||
uid: 'userIdString',
|
||||
displayName: 'John Snow',
|
||||
username: 'johnSnow01',
|
||||
isAdmin: false,
|
||||
@@ -166,7 +162,7 @@ export class UserController {
|
||||
})
|
||||
@Patch('{userId}')
|
||||
public async updateUser(
|
||||
@Path() userId: number,
|
||||
@Path() userId: string,
|
||||
@Body() body: UserPayload
|
||||
): Promise<UserDetailsResponse> {
|
||||
const { MODE } = process.env
|
||||
@@ -174,7 +170,7 @@ export class UserController {
|
||||
if (MODE === ModeType.Desktop)
|
||||
return updateDesktopAutoExec(body.autoExec ?? '')
|
||||
|
||||
return updateUser({ id: userId }, body)
|
||||
return updateUser({ _id: userId }, body)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,18 +194,16 @@ export class UserController {
|
||||
*/
|
||||
@Delete('{userId}')
|
||||
public async deleteUser(
|
||||
@Path() userId: number,
|
||||
@Path() userId: string,
|
||||
@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({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
|
||||
.exec()
|
||||
await User.find({}).select('uid username displayName isAdmin').exec()
|
||||
|
||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
||||
@@ -239,15 +233,15 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
|
||||
const groupController = new GroupController()
|
||||
const allUsersGroup = await groupController
|
||||
.getGroupByGroupName(ALL_USERS_GROUP.name)
|
||||
.getGroupByName(ALL_USERS_GROUP.name)
|
||||
.catch(() => {})
|
||||
|
||||
if (allUsersGroup) {
|
||||
await groupController.addUserToGroup(allUsersGroup.groupId, savedUser.id)
|
||||
await groupController.addUserToGroup(allUsersGroup.uid, savedUser.uid)
|
||||
}
|
||||
|
||||
return {
|
||||
id: savedUser.id,
|
||||
uid: savedUser.uid,
|
||||
displayName: savedUser.displayName,
|
||||
username: savedUser.username,
|
||||
isActive: savedUser.isActive,
|
||||
@@ -256,8 +250,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
}
|
||||
}
|
||||
|
||||
interface GetUserBy {
|
||||
id?: number
|
||||
export interface GetUserBy {
|
||||
_id?: string
|
||||
username?: string
|
||||
}
|
||||
|
||||
@@ -267,10 +261,10 @@ const getUser = async (
|
||||
): Promise<UserDetailsResponse> => {
|
||||
const user = (await User.findOne(
|
||||
findBy,
|
||||
`id displayName username isActive isAdmin autoExec -_id`
|
||||
`uid displayName username isActive isAdmin autoExec`
|
||||
).populate(
|
||||
'groups',
|
||||
'groupId name description -_id'
|
||||
'uid name description'
|
||||
)) as unknown as UserDetailsResponse
|
||||
|
||||
if (!user)
|
||||
@@ -280,12 +274,12 @@ const getUser = async (
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
uid: user.uid,
|
||||
displayName: user.displayName,
|
||||
username: user.username,
|
||||
isActive: user.isActive,
|
||||
isAdmin: user.isAdmin,
|
||||
autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
|
||||
autoExec: getAutoExec ? (user.autoExec ?? '') : undefined,
|
||||
groups: user.groups
|
||||
}
|
||||
}
|
||||
@@ -293,7 +287,7 @@ const getUser = async (
|
||||
const getDesktopAutoExec = async () => {
|
||||
return {
|
||||
...desktopUser,
|
||||
id: desktopUser.userId,
|
||||
uid: desktopUser.userId,
|
||||
autoExec: await getUserAutoExec()
|
||||
}
|
||||
}
|
||||
@@ -329,8 +323,8 @@ const updateUser = async (
|
||||
const usernameExist = await User.findOne({ username })
|
||||
if (usernameExist) {
|
||||
if (
|
||||
(findBy.id && usernameExist.id != findBy.id) ||
|
||||
(findBy.username && usernameExist.username != findBy.username)
|
||||
(findBy._id && usernameExist.uid !== findBy._id) ||
|
||||
(findBy.username && usernameExist.username !== findBy.username)
|
||||
)
|
||||
throw {
|
||||
code: 409,
|
||||
@@ -350,11 +344,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 {
|
||||
id: updatedUser.id,
|
||||
uid: updatedUser.uid,
|
||||
username: updatedUser.username,
|
||||
displayName: updatedUser.displayName,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
@@ -367,7 +361,7 @@ const updateDesktopAutoExec = async (autoExec: string) => {
|
||||
await updateUserAutoExec(autoExec)
|
||||
return {
|
||||
...desktopUser,
|
||||
id: desktopUser.userId,
|
||||
uid: desktopUser.userId,
|
||||
autoExec
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,8 +8,8 @@ export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
|
||||
if (!user?.isAdmin) {
|
||||
let adminAccountRequired: boolean = true
|
||||
|
||||
if (req.params.userId) {
|
||||
adminAccountRequired = user?.userId !== parseInt(req.params.userId)
|
||||
if (req.params.uid) {
|
||||
adminAccountRequired = user?.userId !== req.params.uid
|
||||
} else if (req.params.username) {
|
||||
adminAccountRequired = user?.username !== req.params.username
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
import { Schema, model, Document, Model } from 'mongoose'
|
||||
import { GroupDetailsResponse } from '../controllers'
|
||||
import User, { IUser } from './User'
|
||||
import { AuthProviderType } from '../utils'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
export const PUBLIC_GROUP_NAME = 'Public'
|
||||
export const PUBLIC_GROUP_NAME = 'public'
|
||||
|
||||
export interface GroupPayload {
|
||||
/**
|
||||
@@ -25,10 +24,12 @@ 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 {
|
||||
@@ -38,32 +39,46 @@ interface IGroup extends IGroupDocument {
|
||||
}
|
||||
interface IGroupModel extends Model<IGroup> {}
|
||||
|
||||
const groupSchema = new Schema<IGroupDocument>({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
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' }]
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'Group description.'
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: AuthProviderType
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
|
||||
opts
|
||||
)
|
||||
|
||||
groupSchema.virtual('uid').get(function () {
|
||||
return this._id.toString()
|
||||
})
|
||||
|
||||
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
|
||||
|
||||
// Hooks
|
||||
groupSchema.post('save', function (group: IGroup, next: Function) {
|
||||
group.populate('users', 'id username displayName -_id').then(function () {
|
||||
group.populate('users', 'uid username displayName').then(function () {
|
||||
next()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import { Schema, model, Document, Model } from 'mongoose'
|
||||
import { PermissionDetailsResponse } from '../controllers'
|
||||
|
||||
interface GetPermissionBy {
|
||||
@@ -11,9 +10,11 @@ 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 {}
|
||||
@@ -22,44 +23,54 @@ interface IPermissionModel extends Model<IPermission> {
|
||||
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
|
||||
}
|
||||
|
||||
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 opts = {
|
||||
toJSON: {
|
||||
virtuals: true,
|
||||
transform: function (doc: any, ret: any, options: any) {
|
||||
delete ret._id
|
||||
delete ret.id
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
||||
const permissionSchema = new Schema<IPermissionDocument>(
|
||||
{
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
setting: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: { type: Schema.Types.ObjectId, ref: 'User' },
|
||||
group: { type: Schema.Types.ObjectId, ref: 'Group' }
|
||||
},
|
||||
opts
|
||||
)
|
||||
|
||||
permissionSchema.virtual('uid').get(function () {
|
||||
return this._id.toString()
|
||||
})
|
||||
|
||||
// Static Methods
|
||||
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
|
||||
PermissionDetailsResponse[]
|
||||
> {
|
||||
return (await this.find(getBy)
|
||||
.select({
|
||||
_id: 0,
|
||||
permissionId: 1,
|
||||
path: 1,
|
||||
type: 1,
|
||||
setting: 1
|
||||
})
|
||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||
.select('uid path type setting')
|
||||
.populate({ path: 'user', select: 'uid username displayName isAdmin' })
|
||||
.populate({
|
||||
path: 'group',
|
||||
select: 'groupId name description -_id',
|
||||
select: 'uid name description',
|
||||
populate: {
|
||||
path: 'users',
|
||||
select: 'id username displayName isAdmin -_id',
|
||||
select: 'uid username displayName isAdmin',
|
||||
options: { limit: 15 }
|
||||
}
|
||||
})) as unknown as PermissionDetailsResponse[]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import { Schema, model, Document, Model, ObjectId } from 'mongoose'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { AuthProviderType } from '../utils'
|
||||
|
||||
@@ -37,7 +36,6 @@ export interface UserPayload {
|
||||
|
||||
interface IUserDocument extends UserPayload, Document {
|
||||
_id: Schema.Types.ObjectId
|
||||
id: number
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
needsToUpdatePassword: boolean
|
||||
@@ -45,6 +43,9 @@ 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 {
|
||||
@@ -55,59 +56,75 @@ export interface IUser extends IUserDocument {
|
||||
interface IUserModel extends Model<IUser> {
|
||||
hashPassword(password: string): string
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
opts
|
||||
)
|
||||
|
||||
userSchema.virtual('uid').get(function () {
|
||||
return this._id.toString()
|
||||
})
|
||||
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
|
||||
|
||||
// Static Methods
|
||||
userSchema.static('hashPassword', (password: string): string => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express'
|
||||
import { runCodeValidation } from '../../utils'
|
||||
import { runCodeValidation, triggerCodeValidation } from '../../utils'
|
||||
import { CodeController } from '../../controllers/'
|
||||
|
||||
const runRouter = express.Router()
|
||||
@@ -28,4 +28,22 @@ runRouter.post('/execute', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
runRouter.post('/trigger', async (req, res) => {
|
||||
const { error, value: body } = triggerCodeValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.triggerCode(req, body)
|
||||
|
||||
res.status(200)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default runRouter
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import express from 'express'
|
||||
import { GroupController } from '../../controllers/'
|
||||
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
||||
import { getGroupValidation, registerGroupValidation } from '../../utils'
|
||||
import {
|
||||
getGroupValidation,
|
||||
registerGroupValidation,
|
||||
uidValidation
|
||||
} from '../../utils'
|
||||
|
||||
const groupRouter = express.Router()
|
||||
|
||||
@@ -33,12 +37,15 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
|
||||
const { groupId } = req.params
|
||||
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
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.getGroup(parseInt(groupId))
|
||||
const response = await controller.getGroup(uid)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
@@ -56,7 +63,7 @@ groupRouter.get(
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.getGroupByGroupName(name)
|
||||
const response = await controller.getGroupByName(name)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
@@ -65,18 +72,15 @@ groupRouter.get(
|
||||
)
|
||||
|
||||
groupRouter.post(
|
||||
'/:groupId/:userId',
|
||||
'/:groupUid/:userUid',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
const { groupUid, userUid } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.addUserToGroup(
|
||||
parseInt(groupId),
|
||||
parseInt(userId)
|
||||
)
|
||||
const response = await controller.addUserToGroup(groupUid, userUid)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
@@ -85,18 +89,15 @@ groupRouter.post(
|
||||
)
|
||||
|
||||
groupRouter.delete(
|
||||
'/:groupId/:userId',
|
||||
'/:groupUid/:userUid',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
const { groupUid, userUid } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.removeUserFromGroup(
|
||||
parseInt(groupId),
|
||||
parseInt(userId)
|
||||
)
|
||||
const response = await controller.removeUserFromGroup(groupUid, userUid)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
@@ -105,15 +106,18 @@ groupRouter.delete(
|
||||
)
|
||||
|
||||
groupRouter.delete(
|
||||
'/:groupId',
|
||||
'/:uid',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req, res) => {
|
||||
const { groupId } = req.params
|
||||
const { error: uidError, value: params } = uidValidation(req.params)
|
||||
if (uidError) return res.status(400).send(uidError.details[0].message)
|
||||
|
||||
const { uid } = params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
await controller.deleteGroup(parseInt(groupId))
|
||||
await controller.deleteGroup(uid)
|
||||
res.status(200).send('Group Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PermissionController } from '../../controllers/'
|
||||
import { verifyAdmin } from '../../middlewares'
|
||||
import {
|
||||
registerPermissionValidation,
|
||||
uidValidation,
|
||||
updatePermissionValidation
|
||||
} from '../../utils'
|
||||
|
||||
@@ -34,14 +35,17 @@ permissionRouter.post('/', verifyAdmin, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
|
||||
const { permissionId } = req.params
|
||||
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
|
||||
|
||||
const { error, value: body } = updatePermissionValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.updatePermission(permissionId, body)
|
||||
const response = await controller.updatePermission(uid, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
@@ -50,20 +54,18 @@ permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
permissionRouter.delete(
|
||||
'/:permissionId',
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
const { permissionId } = req.params
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
)
|
||||
})
|
||||
export default permissionRouter
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
generateAccessToken,
|
||||
generateAuthCode,
|
||||
generateRefreshToken,
|
||||
randomBytesHexString,
|
||||
saveTokensInDB,
|
||||
verifyTokenInDB
|
||||
} from '../../../utils'
|
||||
@@ -20,7 +21,6 @@ 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: user.id
|
||||
userId: randomBytesHexString(12)
|
||||
}
|
||||
beforeAll(async () => {
|
||||
await userController.createUser(user)
|
||||
@@ -151,10 +151,10 @@ describe('auth', () => {
|
||||
currentUser = await userController.createUser(user)
|
||||
refreshToken = generateRefreshToken({
|
||||
clientId,
|
||||
userId: currentUser.id
|
||||
userId: currentUser.uid
|
||||
})
|
||||
await saveTokensInDB(
|
||||
currentUser.id,
|
||||
currentUser.uid,
|
||||
clientId,
|
||||
'accessToken',
|
||||
refreshToken
|
||||
@@ -202,11 +202,11 @@ describe('auth', () => {
|
||||
currentUser = await userController.createUser(user)
|
||||
accessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId: currentUser.id
|
||||
userId: currentUser.uid
|
||||
})
|
||||
|
||||
await saveTokensInDB(
|
||||
currentUser.id,
|
||||
currentUser.uid,
|
||||
clientId,
|
||||
accessToken,
|
||||
'refreshToken'
|
||||
|
||||
@@ -40,10 +40,10 @@ describe('client', () => {
|
||||
const dbUser = await userController.createUser(adminUser)
|
||||
adminAccessToken = generateAccessToken({
|
||||
clientId: client.clientId,
|
||||
userId: dbUser.id
|
||||
userId: dbUser.uid
|
||||
})
|
||||
await saveTokensInDB(
|
||||
dbUser.id,
|
||||
dbUser.uid,
|
||||
client.clientId,
|
||||
adminAccessToken,
|
||||
'refreshToken'
|
||||
@@ -95,10 +95,10 @@ describe('client', () => {
|
||||
const dbUser = await userController.createUser(user)
|
||||
const accessToken = generateAccessToken({
|
||||
clientId: client.clientId,
|
||||
userId: dbUser.id
|
||||
userId: dbUser.uid
|
||||
})
|
||||
await saveTokensInDB(
|
||||
dbUser.id,
|
||||
dbUser.uid,
|
||||
client.clientId,
|
||||
accessToken,
|
||||
'refreshToken'
|
||||
@@ -212,10 +212,10 @@ describe('client', () => {
|
||||
const dbUser = await userController.createUser(user)
|
||||
const accessToken = generateAccessToken({
|
||||
clientId: client.clientId,
|
||||
userId: dbUser.id
|
||||
userId: dbUser.uid
|
||||
})
|
||||
await saveTokensInDB(
|
||||
dbUser.id,
|
||||
dbUser.uid,
|
||||
client.clientId,
|
||||
accessToken,
|
||||
'refreshToken'
|
||||
|
||||
@@ -71,31 +71,31 @@ describe('drive', () => {
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
const dbUser = await controller.createUser(user)
|
||||
accessToken = await generateAndSaveToken(dbUser.id)
|
||||
accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/deploy',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/deploy/upload',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/file',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/folder',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/rename',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1197,7 +1197,7 @@ const getExampleService = (): ServiceMember =>
|
||||
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
||||
.members[0] as ServiceMember
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const generateAndSaveToken = async (userId: string) => {
|
||||
const adminAccessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 = {
|
||||
@@ -75,7 +76,7 @@ describe('group', () => {
|
||||
.send(group)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
@@ -155,7 +156,7 @@ describe('group', () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
@@ -174,17 +175,17 @@ describe('group', () => {
|
||||
username: 'deletegroup2'
|
||||
})
|
||||
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
|
||||
await groupController.addUserToGroup(dbGroup.uid, dbUser1.uid)
|
||||
await groupController.addUserToGroup(dbGroup.uid, dbUser2.uid)
|
||||
|
||||
await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
const res1 = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.get(`/SASjsApi/user/${dbUser1.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
@@ -192,7 +193,7 @@ describe('group', () => {
|
||||
expect(res1.body.groups).toEqual([])
|
||||
|
||||
const res2 = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser2.id}`)
|
||||
.get(`/SASjsApi/user/${dbUser2.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
@@ -201,8 +202,10 @@ 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/1234`)
|
||||
.delete(`/SASjsApi/group/${hexValue}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -229,7 +232,7 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
@@ -245,15 +248,15 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
it('should respond with group', async () => {
|
||||
const { groupId } = await groupController.createGroup(group)
|
||||
const { uid } = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/group/${groupId}`)
|
||||
.get(`/SASjsApi/group/${uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
@@ -266,15 +269,15 @@ describe('group', () => {
|
||||
username: 'get' + user.username
|
||||
})
|
||||
|
||||
const { groupId } = await groupController.createGroup(group)
|
||||
const { uid } = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/group/${groupId}`)
|
||||
.get(`/SASjsApi/group/${uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
@@ -292,8 +295,10 @@ 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/1234')
|
||||
.get(`/SASjsApi/group/${hexValue}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -312,7 +317,7 @@ describe('group', () => {
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
@@ -333,7 +338,7 @@ describe('group', () => {
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
@@ -379,7 +384,7 @@ describe('group', () => {
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
name: group.name,
|
||||
description: group.description
|
||||
}
|
||||
@@ -401,7 +406,7 @@ describe('group', () => {
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
name: group.name,
|
||||
description: group.description
|
||||
}
|
||||
@@ -426,18 +431,18 @@ describe('group', () => {
|
||||
const dbUser = await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).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([
|
||||
{
|
||||
id: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
}
|
||||
@@ -452,20 +457,20 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser.id}`)
|
||||
.get(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groups).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
name: group.name,
|
||||
description: group.description
|
||||
}
|
||||
@@ -478,21 +483,21 @@ describe('group', () => {
|
||||
...user,
|
||||
username: 'addUserRandomUser'
|
||||
})
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).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([
|
||||
{
|
||||
id: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
username: 'addUserRandomUser',
|
||||
displayName: user.displayName
|
||||
}
|
||||
@@ -526,8 +531,10 @@ 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/123/123')
|
||||
.post(`/SASjsApi/group/${hexValue}/123`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -538,8 +545,10 @@ 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.groupId}/123`)
|
||||
.post(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -556,7 +565,7 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
@@ -577,7 +586,7 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
@@ -596,7 +605,7 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
@@ -618,15 +627,15 @@ describe('group', () => {
|
||||
...user,
|
||||
username: 'removeUserRandomUser'
|
||||
})
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
@@ -639,16 +648,16 @@ describe('group', () => {
|
||||
...user,
|
||||
username: 'removeGroupFromUser'
|
||||
})
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
|
||||
|
||||
await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser.id}`)
|
||||
.get(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
@@ -667,7 +676,7 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
@@ -686,7 +695,7 @@ describe('group', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
@@ -723,8 +732,10 @@ 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/123/123')
|
||||
.delete(`/SASjsApi/group/${hexValue}/123`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -735,8 +746,10 @@ 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.groupId}/123`)
|
||||
.delete(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -752,10 +765,10 @@ const generateSaveTokenAndCreateUser = async (
|
||||
): Promise<string> => {
|
||||
const dbUser = await userController.createUser(someUser ?? adminUser)
|
||||
|
||||
return generateAndSaveToken(dbUser.id)
|
||||
return generateAndSaveToken(dbUser.uid)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const generateAndSaveToken = async (userId: string) => {
|
||||
const adminAccessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
PermissionDetailsResponse
|
||||
} from '../../../controllers'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
const deployPayload = {
|
||||
appLoc: 'string',
|
||||
@@ -103,10 +104,10 @@ describe('permission', () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ ...permission, principalId: dbUser.id })
|
||||
.send({ ...permission, principalId: dbUser.uid })
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.permissionId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.path).toEqual(permission.path)
|
||||
expect(res.body.type).toEqual(permission.type)
|
||||
expect(res.body.setting).toEqual(permission.setting)
|
||||
@@ -122,11 +123,11 @@ describe('permission', () => {
|
||||
.send({
|
||||
...permission,
|
||||
principalType: 'group',
|
||||
principalId: dbGroup.groupId
|
||||
principalId: dbGroup.uid
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.permissionId).toBeTruthy()
|
||||
expect(res.body.uid).toBeTruthy()
|
||||
expect(res.body.path).toEqual(permission.path)
|
||||
expect(res.body.type).toEqual(permission.type)
|
||||
expect(res.body.setting).toEqual(permission.setting)
|
||||
@@ -144,7 +145,7 @@ describe('permission', () => {
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
@@ -281,17 +282,19 @@ describe('permission', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if principalId is not a number', async () => {
|
||||
it('should respond with Bad Request if principalId is not a string of 24 hex characters', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
principalId: 'someCharacters'
|
||||
principalId: randomBytes(10).toString('hex')
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('"principalId" must be a number')
|
||||
expect(res.text).toEqual(
|
||||
'"principalId" length must be 24 characters long'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -307,7 +310,7 @@ describe('permission', () => {
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
principalId: adminUser.id
|
||||
principalId: adminUser.uid
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
@@ -321,7 +324,7 @@ describe('permission', () => {
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
principalId: 123
|
||||
principalId: randomBytes(12).toString('hex')
|
||||
})
|
||||
.expect(404)
|
||||
|
||||
@@ -336,7 +339,7 @@ describe('permission', () => {
|
||||
.send({
|
||||
...permission,
|
||||
principalType: 'group',
|
||||
principalId: 123
|
||||
principalId: randomBytes(12).toString('hex')
|
||||
})
|
||||
.expect(404)
|
||||
|
||||
@@ -347,13 +350,13 @@ describe('permission', () => {
|
||||
it('should respond with Conflict (409) if permission already exists', async () => {
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ ...permission, principalId: dbUser.id })
|
||||
.send({ ...permission, principalId: dbUser.uid })
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
@@ -368,7 +371,7 @@ describe('permission', () => {
|
||||
beforeAll(async () => {
|
||||
dbPermission = await permissionController.createPermission({
|
||||
...permission,
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
})
|
||||
|
||||
@@ -378,7 +381,7 @@ describe('permission', () => {
|
||||
|
||||
it('should respond with updated permission', async () => {
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ setting: PermissionSettingForRoute.deny })
|
||||
.expect(200)
|
||||
@@ -388,7 +391,7 @@ describe('permission', () => {
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
@@ -403,7 +406,7 @@ describe('permission', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
@@ -414,7 +417,7 @@ describe('permission', () => {
|
||||
|
||||
it('should respond with Bad Request if setting is missing', async () => {
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
@@ -425,7 +428,7 @@ describe('permission', () => {
|
||||
|
||||
it('should respond with Bad Request if setting is invalid', async () => {
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
setting: 'invalid'
|
||||
@@ -437,8 +440,9 @@ 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/123')
|
||||
.patch(`/SASjsApi/permission/${hexValue}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
setting: PermissionSettingForRoute.deny
|
||||
@@ -454,10 +458,10 @@ describe('permission', () => {
|
||||
it('should delete permission', async () => {
|
||||
const dbPermission = await permissionController.createPermission({
|
||||
...permission,
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.delete(`/SASjsApi/permission/${dbPermission?.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
@@ -466,8 +470,10 @@ 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/123')
|
||||
.delete(`/SASjsApi/permission/${hexValue}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -481,12 +487,12 @@ describe('permission', () => {
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/test-1',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/test-2',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
})
|
||||
|
||||
@@ -505,12 +511,12 @@ describe('permission', () => {
|
||||
...user,
|
||||
username: 'get' + user.username
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(nonAdminUser.id)
|
||||
const accessToken = await generateAndSaveToken(nonAdminUser.uid)
|
||||
await permissionController.createPermission({
|
||||
path: '/test-1',
|
||||
type: PermissionType.route,
|
||||
principalType: PrincipalType.user,
|
||||
principalId: nonAdminUser.id,
|
||||
principalId: nonAdminUser.uid,
|
||||
setting: PermissionSettingForRoute.grant
|
||||
})
|
||||
|
||||
@@ -531,7 +537,7 @@ describe('permission', () => {
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/deploy',
|
||||
principalId: dbUser.id
|
||||
principalId: dbUser.uid
|
||||
})
|
||||
})
|
||||
|
||||
@@ -551,7 +557,7 @@ describe('permission', () => {
|
||||
})
|
||||
|
||||
it('should create files in SASJS drive', async () => {
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
await request(app)
|
||||
.get('/SASjsApi/drive/deploy')
|
||||
@@ -561,7 +567,7 @@ describe('permission', () => {
|
||||
})
|
||||
|
||||
it('should respond unauthorized', async () => {
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
await request(app)
|
||||
.get('/SASjsApi/drive/deploy/upload')
|
||||
@@ -577,10 +583,10 @@ const generateSaveTokenAndCreateUser = async (
|
||||
): Promise<string> => {
|
||||
const dbUser = await userController.createUser(someUser ?? adminUser)
|
||||
|
||||
return generateAndSaveToken(dbUser.id)
|
||||
return generateAndSaveToken(dbUser.uid)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const generateAndSaveToken = async (userId: string) => {
|
||||
const adminAccessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
|
||||
@@ -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.id)
|
||||
accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
await permissionController.createPermission({
|
||||
path: '/SASjsApi/stp/execute',
|
||||
type: PermissionType.route,
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
principalId: dbUser.uid,
|
||||
setting: PermissionSettingForRoute.grant
|
||||
})
|
||||
})
|
||||
@@ -456,7 +456,7 @@ const makeRequestAndAssert = async (
|
||||
)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const generateAndSaveToken = async (userId: string) => {
|
||||
const accessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
@@ -101,9 +102,9 @@ describe('user', () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId: dbUser.id
|
||||
userId: dbUser.uid
|
||||
})
|
||||
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
||||
await saveTokensInDB(dbUser.uid, clientId, accessToken, 'refreshToken')
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
@@ -187,7 +188,7 @@ describe('user', () => {
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser.id}`)
|
||||
.patch(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ ...user, displayName: newDisplayName })
|
||||
.expect(200)
|
||||
@@ -200,11 +201,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.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser.id}`)
|
||||
.patch(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({
|
||||
displayName: newDisplayName,
|
||||
@@ -221,11 +222,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.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser.id}`)
|
||||
.patch(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ ...user, displayName: newDisplayName })
|
||||
.expect(400)
|
||||
@@ -277,10 +278,10 @@ describe('user', () => {
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser2.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.patch(`/SASjsApi/user/${dbUser1.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(401)
|
||||
@@ -297,7 +298,7 @@ describe('user', () => {
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.patch(`/SASjsApi/user/${dbUser1.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ username: dbUser2.username })
|
||||
.expect(409)
|
||||
@@ -325,7 +326,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.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
const res = await request(app)
|
||||
@@ -346,7 +347,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.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
await request(app)
|
||||
@@ -372,10 +373,10 @@ describe('user', () => {
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser2.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.patch(`/SASjsApi/user/${dbUser1.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(401)
|
||||
@@ -418,7 +419,7 @@ describe('user', () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
@@ -428,10 +429,10 @@ describe('user', () => {
|
||||
|
||||
it('should respond with OK when user himself requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ password: user.password })
|
||||
.expect(200)
|
||||
@@ -441,10 +442,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.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
@@ -469,10 +470,10 @@ describe('user', () => {
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser2.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.delete(`/SASjsApi/user/${dbUser1.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(401)
|
||||
@@ -483,10 +484,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.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||
.delete(`/SASjsApi/user/${dbUser.uid}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ password: 'incorrectpassword' })
|
||||
.expect(401)
|
||||
@@ -510,7 +511,7 @@ describe('user', () => {
|
||||
|
||||
it('should respond with OK when user himself requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
@@ -523,7 +524,7 @@ describe('user', () => {
|
||||
|
||||
it('should respond with Bad Request when user himself requests and password is missing', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
@@ -551,7 +552,7 @@ describe('user', () => {
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser2.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
||||
@@ -565,7 +566,7 @@ describe('user', () => {
|
||||
|
||||
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
@@ -592,7 +593,7 @@ describe('user', () => {
|
||||
|
||||
it('should respond with user autoExec when same user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
const userId = dbUser.uid
|
||||
const accessToken = await generateAndSaveToken(userId)
|
||||
|
||||
const res = await request(app)
|
||||
@@ -611,7 +612,7 @@ describe('user', () => {
|
||||
|
||||
it('should respond with user autoExec when admin user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
const userId = dbUser.uid
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${userId}`)
|
||||
@@ -634,7 +635,7 @@ describe('user', () => {
|
||||
})
|
||||
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
const userId = dbUser.uid
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${userId}`)
|
||||
@@ -652,7 +653,7 @@ describe('user', () => {
|
||||
|
||||
it('should respond with user along with associated groups', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
const userId = dbUser.uid
|
||||
const accessToken = await generateAndSaveToken(userId)
|
||||
|
||||
const group = {
|
||||
@@ -661,7 +662,7 @@ describe('user', () => {
|
||||
}
|
||||
const groupController = new GroupController()
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${userId}`)
|
||||
@@ -690,8 +691,10 @@ 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/1234')
|
||||
.get(`/SASjsApi/user/${hexValue}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
@@ -703,7 +706,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.id
|
||||
const userId = dbUser.uid
|
||||
const accessToken = await generateAndSaveToken(userId)
|
||||
|
||||
const res = await request(app)
|
||||
@@ -803,13 +806,13 @@ describe('user', () => {
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
id: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
username: adminUser.username,
|
||||
displayName: adminUser.displayName,
|
||||
isAdmin: adminUser.isAdmin
|
||||
},
|
||||
{
|
||||
id: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin
|
||||
@@ -831,13 +834,13 @@ describe('user', () => {
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
id: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
username: adminUser.username,
|
||||
displayName: adminUser.displayName,
|
||||
isAdmin: adminUser.isAdmin
|
||||
},
|
||||
{
|
||||
id: expect.anything(),
|
||||
uid: expect.anything(),
|
||||
username: 'randomUser',
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin
|
||||
@@ -859,10 +862,10 @@ const generateSaveTokenAndCreateUser = async (
|
||||
): Promise<string> => {
|
||||
const dbUser = await controller.createUser(someUser ?? adminUser)
|
||||
|
||||
return generateAndSaveToken(dbUser.id)
|
||||
return generateAndSaveToken(dbUser.uid)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const generateAndSaveToken = async (userId: string) => {
|
||||
const adminAccessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
|
||||
@@ -47,6 +47,77 @@ describe('web', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('SASLogon/authorize', () => {
|
||||
let csrfToken: string
|
||||
let authCookies: string
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ csrfToken } = await getCSRF(app))
|
||||
|
||||
await userController.createUser(user)
|
||||
|
||||
const credentials = {
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
|
||||
;({ authCookies } = await performLogin(app, credentials, csrfToken))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with authorization code', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({ clientId })
|
||||
|
||||
expect(res.body).toHaveProperty('code')
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if CSRF Token is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.send({ clientId })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid CSRF token!')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({
|
||||
clientId: 'WrongClientID'
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SASLogon/login', () => {
|
||||
let csrfToken: string
|
||||
|
||||
@@ -74,7 +145,7 @@ describe('web', () => {
|
||||
|
||||
expect(res.body.loggedIn).toBeTruthy()
|
||||
expect(res.body.user).toEqual({
|
||||
id: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin,
|
||||
@@ -187,78 +258,6 @@ describe('web', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SASLogon/authorize', () => {
|
||||
let csrfToken: string
|
||||
let authCookies: string
|
||||
|
||||
beforeAll(async () => {
|
||||
await deleteDocumentsFromLimitersCollections()
|
||||
;({ csrfToken } = await getCSRF(app))
|
||||
|
||||
await userController.createUser(user)
|
||||
|
||||
const credentials = {
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
|
||||
;({ authCookies } = await performLogin(app, credentials, csrfToken))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with authorization code', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({ clientId })
|
||||
|
||||
expect(res.body).toHaveProperty('code')
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if CSRF Token is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.send({ clientId })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid CSRF token!')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({
|
||||
clientId: 'WrongClientID'
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const getCSRF = async (app: Express) => {
|
||||
@@ -285,12 +284,3 @@ const extractCSRF = (text: string) =>
|
||||
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
||||
text
|
||||
)![1]
|
||||
|
||||
const deleteDocumentsFromLimitersCollections = async () => {
|
||||
const { collections } = mongoose.connection
|
||||
const login_fail_ip_per_day_collection = collections['login_fail_ip_per_day']
|
||||
await login_fail_ip_per_day_collection.deleteMany({})
|
||||
const login_fail_consecutive_username_and_ip_collection =
|
||||
collections['login_fail_consecutive_username_and_ip']
|
||||
await login_fail_consecutive_username_and_ip_collection.deleteMany({})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ stpRouter.get('/execute', async (req, res) => {
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.executeGetRequest(req, query._program)
|
||||
const response = await controller.executeGetRequest(
|
||||
req,
|
||||
query._program,
|
||||
query._debug
|
||||
)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
deleteUserValidation,
|
||||
getUserValidation,
|
||||
registerUserValidation,
|
||||
uidValidation,
|
||||
updateUserValidation
|
||||
} from '../../utils'
|
||||
|
||||
@@ -56,12 +57,15 @@ userRouter.get(
|
||||
}
|
||||
)
|
||||
|
||||
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
|
||||
const { userId } = req.params
|
||||
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
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
const response = await controller.getUser(req, parseInt(userId))
|
||||
const response = await controller.getUser(req, uid)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
@@ -97,12 +101,16 @@ userRouter.patch(
|
||||
)
|
||||
|
||||
userRouter.patch(
|
||||
'/:userId',
|
||||
'/:uid',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req, res) => {
|
||||
const { user } = req
|
||||
const { userId } = req.params
|
||||
|
||||
const { error: uidError, value: params } = uidValidation(req.params)
|
||||
if (uidError) return res.status(400).send(uidError.details[0].message)
|
||||
|
||||
const { uid } = params
|
||||
|
||||
// only an admin can update `isActive` and `isAdmin` fields
|
||||
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
|
||||
@@ -110,7 +118,7 @@ userRouter.patch(
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
const response = await controller.updateUser(parseInt(userId), body)
|
||||
const response = await controller.updateUser(uid, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
@@ -147,12 +155,16 @@ userRouter.delete(
|
||||
)
|
||||
|
||||
userRouter.delete(
|
||||
'/:userId',
|
||||
'/:uid',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req, res) => {
|
||||
const { user } = req
|
||||
const { userId } = req.params
|
||||
|
||||
const { error: uidError, value: params } = uidValidation(req.params)
|
||||
if (uidError) return res.status(400).send(uidError.details[0].message)
|
||||
|
||||
const { uid } = params
|
||||
|
||||
// only an admin can delete user without providing password
|
||||
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
|
||||
@@ -160,7 +172,7 @@ userRouter.delete(
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
||||
await controller.deleteUser(uid, data, user!.isAdmin)
|
||||
res.status(200).send('Account Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(err.code).send(err.message)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface InfoJWT {
|
||||
clientId: string
|
||||
userId: number
|
||||
userId: string
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface PreProgramVars {
|
||||
username: string
|
||||
userId: number
|
||||
userId: string
|
||||
displayName: string
|
||||
serverUrl: string
|
||||
httpHeaders: string[]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface RequestUser {
|
||||
userId: number
|
||||
userId: string
|
||||
clientId: string
|
||||
username: string
|
||||
displayName: string
|
||||
|
||||
@@ -8,4 +8,5 @@ export interface Session {
|
||||
consumed: boolean
|
||||
completed: boolean
|
||||
crashed?: string
|
||||
expiresAfterMins?: { mins: number; used: boolean }
|
||||
}
|
||||
|
||||
4
api/src/utils/crypto.ts
Normal file
4
api/src/utils/crypto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
export const randomBytesHexString = (bytesCount: number) =>
|
||||
randomBytes(bytesCount).toString('hex')
|
||||
@@ -22,7 +22,7 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
||||
//So this is workaround.
|
||||
return {
|
||||
username: user ? user.username : 'demo',
|
||||
userId: user ? user.userId : 0,
|
||||
userId: user ? user.userId : 'demoId',
|
||||
displayName: user ? user.displayName : 'demo',
|
||||
serverUrl: protocol + host,
|
||||
httpHeaders
|
||||
|
||||
@@ -4,7 +4,7 @@ import User from '../model/User'
|
||||
const isValidToken = async (
|
||||
token: string,
|
||||
key: string,
|
||||
userId: number,
|
||||
userId: string,
|
||||
clientId: string
|
||||
) => {
|
||||
const promise = new Promise<boolean>((resolve, reject) =>
|
||||
@@ -22,8 +22,8 @@ const isValidToken = async (
|
||||
return await promise.then(() => true).catch(() => false)
|
||||
}
|
||||
|
||||
export const getTokensFromDB = async (userId: number, clientId: string) => {
|
||||
const user = await User.findOne({ id: userId })
|
||||
export const getTokensFromDB = async (userId: string, clientId: string) => {
|
||||
const user = await User.findOne({ _id: userId })
|
||||
if (!user) return
|
||||
|
||||
const currentTokenObj = user.tokens.find(
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './appStreamConfig'
|
||||
export * from './connectDB'
|
||||
export * from './copySASjsCore'
|
||||
export * from './createWeboutSasFile'
|
||||
export * from './crypto'
|
||||
export * from './desktopAutoExec'
|
||||
export * from './extractHeaders'
|
||||
export * from './extractName'
|
||||
|
||||
@@ -22,7 +22,7 @@ export const isPublicRoute = async (req: Request): Promise<boolean> => {
|
||||
}
|
||||
|
||||
export const publicUser: RequestUser = {
|
||||
userId: 0,
|
||||
userId: 'public_user_id',
|
||||
clientId: 'public_app',
|
||||
username: 'publicUser',
|
||||
displayName: 'Public User',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { RateLimiterMongo } from 'rate-limiter-flexible'
|
||||
import { RateLimiterMemory } from 'rate-limiter-flexible'
|
||||
|
||||
export class RateLimiter {
|
||||
private static instance: RateLimiter
|
||||
private limiterSlowBruteByIP: RateLimiterMongo
|
||||
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMongo
|
||||
private limiterSlowBruteByIP: RateLimiterMemory
|
||||
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMemory
|
||||
private maxWrongAttemptsByIpPerDay: number
|
||||
private maxConsecutiveFailsByUsernameAndIp: number
|
||||
|
||||
@@ -19,19 +18,17 @@ export class RateLimiter {
|
||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||
)
|
||||
|
||||
this.limiterSlowBruteByIP = new RateLimiterMongo({
|
||||
storeClient: mongoose.connection,
|
||||
this.limiterSlowBruteByIP = new RateLimiterMemory({
|
||||
keyPrefix: 'login_fail_ip_per_day',
|
||||
points: this.maxWrongAttemptsByIpPerDay,
|
||||
duration: 60 * 60 * 24,
|
||||
blockDuration: 60 * 60 * 24 // Block for 1 day
|
||||
})
|
||||
|
||||
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMongo({
|
||||
storeClient: mongoose.connection,
|
||||
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMemory({
|
||||
keyPrefix: 'login_fail_consecutive_username_and_ip',
|
||||
points: this.maxConsecutiveFailsByUsernameAndIp,
|
||||
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
|
||||
duration: 60 * 60 * 24 * 24, // Store number for 24 days since first fail
|
||||
blockDuration: 60 * 60 // Block for 1 hour
|
||||
})
|
||||
}
|
||||
@@ -60,8 +57,7 @@ export class RateLimiter {
|
||||
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
||||
])
|
||||
|
||||
// NOTE: To make use of blockDuration option from RateLimiterMongo
|
||||
// comparison in both following if statements should have greater than symbol
|
||||
// NOTE: To make use of blockDuration option, comparison in both following if statements should have greater than symbol
|
||||
// otherwise, blockDuration option will not work
|
||||
// For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration
|
||||
|
||||
@@ -103,10 +99,11 @@ export class RateLimiter {
|
||||
if (rlRejected instanceof Error) {
|
||||
throw rlRejected
|
||||
} else {
|
||||
// based upon the implementation of consume method of RateLimiterMongo
|
||||
// based upon the implementation of consume method of RateLimiterMemory
|
||||
// we are sure that rlRejected will contain msBeforeNext
|
||||
// for further reference,
|
||||
// see https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection
|
||||
// or see https://github.com/animir/node-rate-limiter-flexible#ratelimiterres-object
|
||||
return Math.ceil(rlRejected.msBeforeNext / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import User from '../model/User'
|
||||
|
||||
export const removeTokensInDB = async (userId: number, clientId: string) => {
|
||||
const user = await User.findOne({ id: userId })
|
||||
export const removeTokensInDB = async (userId: string, clientId: string) => {
|
||||
const user = await User.findOne({ _id: userId })
|
||||
if (!user) return
|
||||
|
||||
const tokenObjIndex = user.tokens.findIndex(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import User from '../model/User'
|
||||
|
||||
export const saveTokensInDB = async (
|
||||
userId: number,
|
||||
userId: string,
|
||||
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(
|
||||
|
||||
@@ -82,7 +82,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
||||
}
|
||||
|
||||
export const ALL_USERS_GROUP = {
|
||||
name: 'AllUsers',
|
||||
name: 'all-users',
|
||||
description: 'Group contains all users'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import path from 'path'
|
||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||
|
||||
import {
|
||||
createFolder,
|
||||
getAbsolutePath,
|
||||
getRealPath,
|
||||
fileExists
|
||||
} from '@sasjs/utils'
|
||||
import dotenv from 'dotenv'
|
||||
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
|
||||
|
||||
export const setProcessVariables = async () => {
|
||||
const { execPath } = process
|
||||
|
||||
// Check if execPath ends with 'api-macos' to determine executable for MacOS.
|
||||
// This is needed to fix picking .env file issue in MacOS executable.
|
||||
if (execPath) {
|
||||
const envPathSplitted = execPath.split(path.sep)
|
||||
|
||||
if (envPathSplitted.pop() === 'api-macos') {
|
||||
const envPath = path.join(envPathSplitted.join(path.sep), '.env')
|
||||
|
||||
// Override environment variables from envPath if file exists
|
||||
if (await fileExists(envPath)) {
|
||||
dotenv.config({ path: envPath, override: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { MODE, RUN_TIMES } = process.env
|
||||
|
||||
if (MODE === ModeType.Server) {
|
||||
@@ -21,6 +43,7 @@ export const setProcessVariables = async () => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
|
||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,7 +64,9 @@ export const setProcessVariables = async () => {
|
||||
|
||||
const { SASJS_ROOT } = process.env
|
||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||
|
||||
await createFolder(absPath)
|
||||
|
||||
process.sasjsRoot = getRealPath(absPath)
|
||||
|
||||
const { DRIVE_LOCATION } = process.env
|
||||
@@ -49,6 +74,7 @@ export const setProcessVariables = async () => {
|
||||
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
|
||||
process.cwd()
|
||||
)
|
||||
|
||||
await createFolder(absDrivePath)
|
||||
process.driveLoc = getRealPath(absDrivePath)
|
||||
|
||||
@@ -57,7 +83,9 @@ export const setProcessVariables = async () => {
|
||||
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
|
||||
process.cwd()
|
||||
)
|
||||
|
||||
await createFolder(absLogsPath)
|
||||
|
||||
process.logsLoc = getRealPath(absLogsPath)
|
||||
|
||||
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
|
||||
@@ -51,9 +51,8 @@ export const generateFileUploadSasCode = async (
|
||||
let fileCount = 0
|
||||
const uploadedFiles: UploadedFiles[] = []
|
||||
|
||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||
sasSessionFolder
|
||||
)
|
||||
const sasSessionFolderList: string[] =
|
||||
await listFilesInFolder(sasSessionFolder)
|
||||
sasSessionFolderList.forEach((fileName) => {
|
||||
let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount
|
||||
fileCountString = fileCount < 10 ? '00' + fileCount : fileCount
|
||||
|
||||
@@ -12,6 +12,11 @@ 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()
|
||||
@@ -113,7 +118,7 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||
principalType: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PrincipalType)),
|
||||
principalId: Joi.number().required()
|
||||
principalId: Joi.string().length(24).hex().required()
|
||||
}).validate(data)
|
||||
|
||||
export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
|
||||
@@ -178,9 +183,17 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
runTime: Joi.string().valid(...process.runTimes)
|
||||
}).validate(data)
|
||||
|
||||
export const triggerCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
code: Joi.string().required(),
|
||||
runTime: Joi.string().valid(...process.runTimes),
|
||||
expiresAfterMins: Joi.number().greater(0)
|
||||
}).validate(data)
|
||||
|
||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_program: Joi.string().required()
|
||||
_program: Joi.string().required(),
|
||||
_debug: Joi.number()
|
||||
})
|
||||
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
|
||||
.validate(data)
|
||||
|
||||
@@ -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: number,
|
||||
userId: string,
|
||||
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
|
||||
|
||||
|
||||
937
web/package-lock.json
generated
937
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-monaco-editor": "^0.48.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-toastify": "^9.0.1"
|
||||
@@ -41,6 +42,7 @@
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-highlight": "^0.12.5",
|
||||
"@types/react-router-dom": "^5.3.1",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-prismjs": "^2.1.0",
|
||||
@@ -59,6 +61,7 @@
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.5.2",
|
||||
"typescript-plugin-css-modules": "^5.0.1",
|
||||
"webpack": "5.64.3",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "4.7.4"
|
||||
|
||||
@@ -3,12 +3,11 @@ import Snackbar from '@mui/material/Snackbar'
|
||||
import MuiAlert, { AlertProps } from '@mui/material/Alert'
|
||||
import Slide, { SlideProps } from '@mui/material/Slide'
|
||||
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
})
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||
function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
}
|
||||
)
|
||||
|
||||
const Transition = (props: SlideProps) => {
|
||||
return <Slide {...props} direction="up" />
|
||||
|
||||
@@ -99,8 +99,8 @@ const AddPermissionModal = ({
|
||||
principalType: principalType.toLowerCase(),
|
||||
principalId:
|
||||
principalType.toLowerCase() === 'user'
|
||||
? userPrincipal?.id
|
||||
: groupPrincipal?.groupId
|
||||
? userPrincipal?.uid
|
||||
: groupPrincipal?.uid
|
||||
}
|
||||
|
||||
permissions.push(addPermissionPayload)
|
||||
|
||||
@@ -61,7 +61,7 @@ const PermissionTable = ({
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{permissions.map((permission) => (
|
||||
<TableRow key={permission.permissionId}>
|
||||
<TableRow key={permission.uid}>
|
||||
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||
import {
|
||||
findExistingPermission,
|
||||
findUpdatingPermission
|
||||
} from '../../../../utils/helper'
|
||||
} from '../../../../utils'
|
||||
|
||||
const useAddPermission = () => {
|
||||
const {
|
||||
@@ -69,7 +69,7 @@ const useAddPermission = () => {
|
||||
|
||||
for (const permission of updatingPermissions) {
|
||||
await axios
|
||||
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
|
||||
.patch(`/SASjsApi/permission/${permission.uid}`, {
|
||||
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
|
||||
})
|
||||
.then((res) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const useDeletePermissionModal = () => {
|
||||
setDeleteConfirmationModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||
.delete(`/SASjsApi/permission/${selectedPermission?.uid}`)
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission deleted!')
|
||||
|
||||
@@ -62,21 +62,17 @@ const useFilterPermissions = () => {
|
||||
: permissions
|
||||
|
||||
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
||||
principalFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
principalFilteredPermissions.some((item) => item.uid === permission.uid)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
principalTypeFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
(item) => item.uid === permission.uid
|
||||
)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
settingFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
settingFilteredPermissions.some((item) => item.uid === permission.uid)
|
||||
)
|
||||
|
||||
setFilteredPermissions(filteredArray)
|
||||
|
||||
@@ -24,7 +24,7 @@ const useUpdatePermissionModal = () => {
|
||||
setUpdatePermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
||||
.patch(`/SASjsApi/permission/${selectedPermission?.uid}`, {
|
||||
setting
|
||||
})
|
||||
.then((res: any) => {
|
||||
|
||||
@@ -26,18 +26,20 @@ const Profile = () => {
|
||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.get(`/SASjsApi/user/${appContext.userId}`)
|
||||
.then((res: any) => {
|
||||
setUser(res.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
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)
|
||||
})
|
||||
}
|
||||
}, [appContext.userId])
|
||||
|
||||
const handleChange = (event: any) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
import {
|
||||
Backdrop,
|
||||
@@ -17,10 +17,14 @@ import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||
import FilePathInputModal from '../../components/filePathInputModal'
|
||||
import FileMenu from './internal/components/fileMenu'
|
||||
import RunMenu from './internal/components/runMenu'
|
||||
import LogComponent from './internal/components/log/logComponent'
|
||||
import LogTabWithIcons from './internal/components/log/logTabWithIcons'
|
||||
|
||||
import { usePrompt } from '../../utils/hooks'
|
||||
import { getLanguageFromExtension } from './internal/helper'
|
||||
import useEditor from './internal/hooks/useEditor'
|
||||
import { RunTimeType } from '../../context/appContext'
|
||||
import { LogObject } from '../../utils'
|
||||
|
||||
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||
padding: '10px'
|
||||
@@ -58,6 +62,7 @@ const SASjsEditor = ({
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
printOutput,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
@@ -108,6 +113,10 @@ const SASjsEditor = ({
|
||||
/>
|
||||
)
|
||||
|
||||
// INFO: variable indicating if selected run type is SAS if there are any errors or warnings in the log
|
||||
const logWithErrorsOrWarnings =
|
||||
selectedRunTime === RunTimeType.SAS && log && typeof log === 'object'
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||
<Backdrop
|
||||
@@ -145,15 +154,35 @@ const SASjsEditor = ({
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<StyledTab label="Code" value="code" />
|
||||
<StyledTab label="Log" value="log" />
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Typography>Webout</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
value="webout"
|
||||
/>
|
||||
{log && (
|
||||
<StyledTab
|
||||
label={logWithErrorsOrWarnings ? '' : 'log'}
|
||||
value="log"
|
||||
icon={
|
||||
logWithErrorsOrWarnings ? (
|
||||
<LogTabWithIcons log={log as LogObject} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const logWrapper = document.querySelector(`#logWrapper`)
|
||||
|
||||
if (logWrapper) logWrapper.scrollTop = 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{webout && (
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Typography>Webout</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
value="webout"
|
||||
/>
|
||||
)}
|
||||
{printOutput && <StyledTab label="print" value="printOutput" />}
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
@@ -195,21 +224,24 @@ const SASjsEditor = ({
|
||||
</Paper>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="log">
|
||||
<div>
|
||||
<h2>Log</h2>
|
||||
<pre
|
||||
id="log"
|
||||
style={{ overflow: 'auto', height: 'calc(100vh - 220px)' }}
|
||||
>
|
||||
{log}
|
||||
</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
{log && (
|
||||
<LogComponent log={log} selectedRunTime={selectedRunTime} />
|
||||
)}
|
||||
</StyledTabPanel>
|
||||
{webout && (
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
)}
|
||||
{printOutput && (
|
||||
<StyledTabPanel value="printOutput">
|
||||
<div>
|
||||
<pre>{printOutput}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
)}
|
||||
</TabContext>
|
||||
)}
|
||||
<Dialog />
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
.ChunkHeader {
|
||||
color: #444;
|
||||
cursor: pointer;
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: 0.4s;
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.2) 0px 2px 1px -1px,
|
||||
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px,
|
||||
rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
|
||||
}
|
||||
|
||||
.ChunkDetails {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ChunkExpandIcon {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ChunkBody {
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ChunksContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.LogContainer {
|
||||
background-color: #fbfbfb;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 3px;
|
||||
min-height: 50px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
font-family: Monaco, Courier, monospace;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.LogWrapper {
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 130px);
|
||||
}
|
||||
|
||||
.LogBody {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
|
||||
.TreeContainer {
|
||||
background-color: white;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.TabContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.TabDownloadIcon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.HighlightedLine {
|
||||
background-color: #f6e30599;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
.GreenIcon {
|
||||
color: green;
|
||||
}
|
||||
171
web/src/containers/Studio/internal/components/log/logChunk.tsx
Normal file
171
web/src/containers/Studio/internal/components/log/logChunk.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { useState, useEffect, SyntheticEvent } from 'react'
|
||||
import { Typography } from '@mui/material'
|
||||
import Highlight from 'react-highlight'
|
||||
import { ErrorOutline, Warning } from '@mui/icons-material'
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import CheckIcon from '@mui/icons-material/Check'
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||
import {
|
||||
defaultChunkSize,
|
||||
parseErrorsAndWarnings,
|
||||
LogInstance,
|
||||
clearErrorsAndWarningsHtmlWrapping,
|
||||
download
|
||||
} from '../../../../../utils'
|
||||
import { logStyles } from './logComponent'
|
||||
import classes from './log.module.css'
|
||||
|
||||
interface LogChunkProps {
|
||||
id: number
|
||||
text: string
|
||||
expanded: boolean
|
||||
logLineCount: number
|
||||
onClick: (evt: any, id: number) => void
|
||||
scrollToLogInstance?: LogInstance
|
||||
updated: number
|
||||
}
|
||||
|
||||
const LogChunk = (props: LogChunkProps) => {
|
||||
const { id, text, logLineCount } = props
|
||||
const [scrollToLogInstance, setScrollToLogInstance] = useState(
|
||||
props.scrollToLogInstance
|
||||
)
|
||||
const rowText = clearErrorsAndWarningsHtmlWrapping(text)
|
||||
const styles = logStyles()
|
||||
const [expanded, setExpanded] = useState(props.expanded)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setExpanded(props.expanded)
|
||||
}, [props.expanded])
|
||||
|
||||
useEffect(() => {
|
||||
if (props.expanded !== expanded) {
|
||||
setExpanded(props.expanded)
|
||||
}
|
||||
|
||||
if (
|
||||
props.scrollToLogInstance &&
|
||||
props.scrollToLogInstance !== scrollToLogInstance
|
||||
) {
|
||||
setScrollToLogInstance(props.scrollToLogInstance)
|
||||
}
|
||||
}, [props])
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded && scrollToLogInstance) {
|
||||
const { type, id } = scrollToLogInstance
|
||||
const line = document.getElementById(`${type}_${id}`)
|
||||
const logWrapper: HTMLDivElement | null =
|
||||
document.querySelector(`#logWrapper`)
|
||||
const logContainer: HTMLHeadElement | null =
|
||||
document.querySelector(`#log_container`)
|
||||
|
||||
if (line && logWrapper && logContainer) {
|
||||
line.className = classes.HighlightedLine
|
||||
|
||||
line.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
|
||||
setTimeout(() => {
|
||||
line.classList.remove(classes.HighlightedLine)
|
||||
|
||||
setScrollToLogInstance(undefined)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}, [expanded, scrollToLogInstance, props])
|
||||
|
||||
const { errors, warnings } = parseErrorsAndWarnings(text)
|
||||
|
||||
const getLineRange = (separator = ' ... ') =>
|
||||
`${id * defaultChunkSize}${separator}${
|
||||
(id + 1) * defaultChunkSize < logLineCount
|
||||
? (id + 1) * defaultChunkSize
|
||||
: logLineCount
|
||||
}`
|
||||
|
||||
return (
|
||||
<div onClick={(evt) => props.onClick(evt, id)}>
|
||||
<button className={classes.ChunkHeader}>
|
||||
<Typography variant="subtitle1">
|
||||
<div className={classes.ChunkDetails}>
|
||||
<span>{`Lines: ${getLineRange()}`}</span>
|
||||
{copied ? (
|
||||
<CheckIcon
|
||||
className={[classes.Icon, classes.GreenIcon].join(' ')}
|
||||
/>
|
||||
) : (
|
||||
<ContentCopyIcon
|
||||
className={classes.Icon}
|
||||
onClick={(evt: SyntheticEvent) => {
|
||||
evt.stopPropagation()
|
||||
|
||||
navigator.clipboard.writeText(rowText)
|
||||
|
||||
setCopied(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FileDownloadIcon
|
||||
onClick={(evt: SyntheticEvent) => {
|
||||
download(evt, rowText, `.${getLineRange('-')}`)
|
||||
}}
|
||||
/>
|
||||
{errors && errors.length !== 0 && (
|
||||
<ErrorOutline
|
||||
color="error"
|
||||
className={classes.Icon}
|
||||
onClick={() => {
|
||||
setScrollToLogInstance(errors[0])
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{warnings && warnings.length !== 0 && (
|
||||
<Warning
|
||||
className={[classes.Icon, classes.GreenIcon].join(' ')}
|
||||
onClick={(evt) => {
|
||||
if (expanded) evt.stopPropagation()
|
||||
|
||||
setScrollToLogInstance(warnings[0])
|
||||
}}
|
||||
/>
|
||||
)}{' '}
|
||||
<ExpandMoreIcon
|
||||
className={classes.ChunkExpandIcon}
|
||||
style={{
|
||||
transform: expanded ? 'rotate(180deg)' : 'unset'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Typography>
|
||||
</button>
|
||||
<div
|
||||
className={classes.ChunkBody}
|
||||
style={{
|
||||
display: expanded ? 'block' : 'none'
|
||||
}}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id={`log_container`}
|
||||
className={[styles.expansionDescription, classes.LogContainer].join(
|
||||
' '
|
||||
)}
|
||||
>
|
||||
<Highlight className={'html'} innerHTML={true}>
|
||||
{expanded ? text : ''}
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogChunk
|
||||
@@ -0,0 +1,243 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import TreeView from '@mui/lab/TreeView'
|
||||
import TreeItem from '@mui/lab/TreeItem'
|
||||
import { ChevronRight, ExpandMore } from '@mui/icons-material'
|
||||
import { Typography } from '@mui/material'
|
||||
import { ListItemText } from '@mui/material'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import Highlight from 'react-highlight'
|
||||
import { LogObject, defaultChunkSize } from '../../../../../utils'
|
||||
import { RunTimeType } from '../../../../../context/appContext'
|
||||
import { splitIntoChunks, LogInstance } from '../../../../../utils'
|
||||
import LogChunk from './logChunk'
|
||||
import classes from './log.module.css'
|
||||
|
||||
export const logStyles: any = makeStyles((theme: any) => ({
|
||||
expansionDescription: {
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
fontSize: theme.typography.pxToRem(12)
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: theme.typography.pxToRem(16)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
interface LogComponentProps {
|
||||
log: LogObject | string
|
||||
selectedRunTime: RunTimeType | string
|
||||
}
|
||||
|
||||
const LogComponent = (props: LogComponentProps) => {
|
||||
const { log, selectedRunTime } = props
|
||||
const logObject = log as LogObject
|
||||
const logChunks = splitIntoChunks(logObject?.body || '')
|
||||
const [logChunksState, setLogChunksState] = useState<boolean[]>(
|
||||
new Array(logChunks.length).fill(false)
|
||||
)
|
||||
const [scrollToLogInstance, setScrollToLogInstance] = useState<LogInstance>()
|
||||
const [oldestExpandedChunk, setOldestExpandedChunk] = useState<number>(
|
||||
logChunksState.length - 1
|
||||
)
|
||||
const maxOpenedChunks = 2
|
||||
|
||||
const styles = logStyles()
|
||||
|
||||
const goToLogLine = (logInstance: LogInstance, ind: number) => {
|
||||
let chunkNumber = 0
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i <= Math.ceil(logObject.linesCount / defaultChunkSize);
|
||||
i++
|
||||
) {
|
||||
if (logInstance.line < (i + 1) * defaultChunkSize) {
|
||||
chunkNumber = i
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setLogChunksState((prevState) => {
|
||||
const newState = [...prevState]
|
||||
newState[chunkNumber] = true
|
||||
|
||||
const chunkToCollapse = getChunkToAutoCollapse()
|
||||
|
||||
if (chunkToCollapse !== undefined) {
|
||||
newState[chunkToCollapse] = false
|
||||
}
|
||||
|
||||
return newState
|
||||
})
|
||||
|
||||
setScrollToLogInstance(logInstance)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// INFO: expand the last chunk by default
|
||||
setLogChunksState((prevState) => {
|
||||
const lastChunk = prevState.length - 1
|
||||
|
||||
const newState = [...prevState]
|
||||
newState[lastChunk] = true
|
||||
|
||||
return newState
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToTheBottom()
|
||||
}, 100)
|
||||
}, [])
|
||||
|
||||
// INFO: scroll to the bottom of the log
|
||||
const scrollToTheBottom = () => {
|
||||
const logWrapper: HTMLDivElement | null =
|
||||
document.querySelector(`#logWrapper`)
|
||||
|
||||
if (logWrapper) {
|
||||
logWrapper.scrollTop = logWrapper.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
const getChunkToAutoCollapse = () => {
|
||||
const openedChunks = logChunksState
|
||||
.map((chunkState: boolean, id: number) => (chunkState ? id : undefined))
|
||||
.filter((chunk) => chunk !== undefined)
|
||||
|
||||
if (openedChunks.length < maxOpenedChunks) return undefined
|
||||
else {
|
||||
const chunkToCollapse = oldestExpandedChunk
|
||||
const newOldestChunk = openedChunks.filter(
|
||||
(chunk) => chunk !== chunkToCollapse
|
||||
)[0]
|
||||
|
||||
if (newOldestChunk !== undefined) {
|
||||
setOldestExpandedChunk(newOldestChunk)
|
||||
|
||||
return chunkToCollapse
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const hasErrorsOrWarnings =
|
||||
logObject.errors?.length !== 0 || logObject.warnings?.length !== 0
|
||||
const logBody = typeof log === 'string' ? log : log.body
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedRunTime === RunTimeType.SAS && logObject.body ? (
|
||||
<div id="logWrapper" className={classes.LogWrapper}>
|
||||
<div>
|
||||
{hasErrorsOrWarnings && (
|
||||
<div className={classes.TreeContainer}>
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMore />}
|
||||
defaultExpandIcon={<ChevronRight />}
|
||||
>
|
||||
{logObject.errors && logObject.errors.length !== 0 && (
|
||||
<TreeItem
|
||||
nodeId="errors"
|
||||
label={
|
||||
<Typography color="error">
|
||||
{`Errors (${logObject.errors.length})`}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
{logObject.errors &&
|
||||
logObject.errors.map((error, ind) => (
|
||||
<TreeItem
|
||||
nodeId={`error_${ind}`}
|
||||
label={<ListItemText primary={error.body} />}
|
||||
key={`error_${ind}`}
|
||||
onClick={() => goToLogLine(error, ind)}
|
||||
/>
|
||||
))}
|
||||
</TreeItem>
|
||||
)}
|
||||
{logObject.warnings && logObject.warnings.length !== 0 && (
|
||||
<TreeItem
|
||||
nodeId="warnings"
|
||||
label={
|
||||
<Typography>{`Warnings (${logObject.warnings.length})`}</Typography>
|
||||
}
|
||||
>
|
||||
{logObject.warnings &&
|
||||
logObject.warnings.map((warning, ind) => (
|
||||
<TreeItem
|
||||
nodeId={`warning_${ind}`}
|
||||
label={<ListItemText primary={warning.body} />}
|
||||
key={`warning_${ind}`}
|
||||
onClick={() => goToLogLine(warning, ind)}
|
||||
/>
|
||||
))}
|
||||
</TreeItem>
|
||||
)}
|
||||
</TreeView>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.ChunksContainer}>
|
||||
{Array.isArray(logChunks) ? (
|
||||
logChunks.map((chunk: string, id: number) => (
|
||||
<LogChunk
|
||||
id={id}
|
||||
text={chunk}
|
||||
expanded={logChunksState[id]}
|
||||
key={`log-chunk-${id}`}
|
||||
logLineCount={logObject.linesCount}
|
||||
scrollToLogInstance={scrollToLogInstance}
|
||||
updated={Date.now()}
|
||||
onClick={(_, chunkNumber) => {
|
||||
setLogChunksState((prevState) => {
|
||||
const newState = [...prevState]
|
||||
const expand = !newState[chunkNumber]
|
||||
|
||||
newState[chunkNumber] = expand
|
||||
|
||||
if (expand) {
|
||||
const chunkToCollapse = getChunkToAutoCollapse()
|
||||
|
||||
if (chunkToCollapse !== undefined) {
|
||||
newState[chunkToCollapse] = false
|
||||
}
|
||||
}
|
||||
|
||||
return newState
|
||||
})
|
||||
|
||||
setScrollToLogInstance(undefined)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Typography
|
||||
id={`log_container`}
|
||||
variant="h5"
|
||||
className={[
|
||||
styles.expansionDescription,
|
||||
classes.LogContainer
|
||||
].join(' ')}
|
||||
>
|
||||
<Highlight className={'html'} innerHTML={true}>
|
||||
{logChunks}
|
||||
</Highlight>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h2>Log</h2>
|
||||
<pre id="log" className={classes.LogBody}>
|
||||
{logBody}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogComponent
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ErrorOutline, Warning } from '@mui/icons-material'
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||
import {
|
||||
LogObject,
|
||||
download,
|
||||
clearErrorsAndWarningsHtmlWrapping
|
||||
} from '../../../../../utils'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import classes from './log.module.css'
|
||||
|
||||
interface LogTabProps {
|
||||
log: LogObject
|
||||
}
|
||||
|
||||
const LogTabWithIcons = (props: LogTabProps) => {
|
||||
const { errors, warnings, body } = props.log
|
||||
|
||||
return (
|
||||
<div className={classes.TabContainer}>
|
||||
<span>log</span>
|
||||
{errors && errors.length !== 0 && (
|
||||
<ErrorOutline color="error" className={classes.Icon} />
|
||||
)}
|
||||
{warnings && warnings.length !== 0 && (
|
||||
<Warning className={[classes.Icon, classes.GreenIcon].join(' ')} />
|
||||
)}
|
||||
<Tooltip
|
||||
title="Download entire log"
|
||||
onClick={(evt) => {
|
||||
download(evt, clearErrorsAndWarningsHtmlWrapping(body))
|
||||
}}
|
||||
>
|
||||
<FileDownloadIcon
|
||||
className={[classes.Icon, classes.TabDownloadIcon].join(' ')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogTabWithIcons
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
useSnackbar,
|
||||
useStateWithCallback
|
||||
} from '../../../../utils/hooks'
|
||||
import { parseErrorsAndWarnings, LogObject } from '../../../../utils'
|
||||
|
||||
const SASJS_LOGS_SEPARATOR =
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
@@ -38,13 +39,15 @@ const useEditor = ({
|
||||
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||
useSnackbar()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [webout, setWebout] = useState('')
|
||||
const [log, setLog] = useState<LogObject | string>()
|
||||
const [webout, setWebout] = useState<string>()
|
||||
const [printOutput, setPrintOutput] = useState<string>()
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType>(
|
||||
RunTimeType.SAS
|
||||
)
|
||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||
const [showDiff, setShowDiff] = useState(false)
|
||||
@@ -150,6 +153,13 @@ const useEditor = ({
|
||||
const runCode = useCallback(
|
||||
(code: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
// Scroll to bottom of log
|
||||
const logElement = document.getElementById('log')
|
||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, {
|
||||
code: programPathInjection(
|
||||
@@ -159,9 +169,30 @@ const useEditor = ({
|
||||
),
|
||||
runTime: selectedRunTime
|
||||
})
|
||||
.then((res: any) => {
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
.then((res: { data: string }) => {
|
||||
// INFO: the order of payload parts is set in @sasjs/server/api/src/controllers/internal/Execution.ts
|
||||
const resDataSplitted = res.data.split(SASJS_LOGS_SEPARATOR)
|
||||
const webout = resDataSplitted[0]
|
||||
const log = resDataSplitted[1]
|
||||
const printOutput = resDataSplitted[2]
|
||||
|
||||
if (selectedRunTime === RunTimeType.SAS) {
|
||||
const { errors, warnings, logLines } = parseErrorsAndWarnings(log)
|
||||
|
||||
const logObject: LogObject = {
|
||||
body: logLines.join(`\n`),
|
||||
errors,
|
||||
warnings,
|
||||
linesCount: logLines.length
|
||||
}
|
||||
|
||||
setLog(logObject)
|
||||
} else {
|
||||
setLog(log)
|
||||
}
|
||||
|
||||
setWebout(webout)
|
||||
setPrintOutput(printOutput)
|
||||
setTab('log')
|
||||
|
||||
// Scroll to bottom of log
|
||||
@@ -249,7 +280,7 @@ const useEditor = ({
|
||||
}, [appContext.runTimes])
|
||||
|
||||
useEffect(() => {
|
||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||
if (runTimes.length) setSelectedRunTime(runTimes[0] as RunTimeType)
|
||||
}, [runTimes])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -280,7 +311,6 @@ const useEditor = ({
|
||||
const content = localStorage.getItem('fileContent') ?? ''
|
||||
setFileContent(content)
|
||||
}
|
||||
setLog('')
|
||||
setWebout('')
|
||||
setTab('code')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -294,7 +324,9 @@ const useEditor = ({
|
||||
|
||||
useEffect(() => {
|
||||
const fileExtension = selectedFileExtension.toLowerCase()
|
||||
if (runTimes.includes(fileExtension)) setSelectedRunTime(fileExtension)
|
||||
|
||||
if (runTimes.includes(fileExtension))
|
||||
setSelectedRunTime(fileExtension as RunTimeType)
|
||||
}, [selectedFileExtension, runTimes])
|
||||
|
||||
return {
|
||||
@@ -308,6 +340,7 @@ const useEditor = ({
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
printOutput,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
|
||||
@@ -24,39 +24,32 @@ export enum RunTimeType {
|
||||
interface AppContextProps {
|
||||
checkingSession: boolean
|
||||
loggedIn: boolean
|
||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||
setLoggedIn?: Dispatch<SetStateAction<boolean>>
|
||||
needsToUpdatePassword: boolean
|
||||
setNeedsToUpdatePassword: Dispatch<SetStateAction<boolean>> | null
|
||||
userId: number
|
||||
setUserId: Dispatch<SetStateAction<number>> | null
|
||||
setNeedsToUpdatePassword?: Dispatch<SetStateAction<boolean>>
|
||||
userId?: string
|
||||
setUserId?: Dispatch<SetStateAction<string | undefined>>
|
||||
username: string
|
||||
setUsername: Dispatch<SetStateAction<string>> | null
|
||||
setUsername?: Dispatch<SetStateAction<string>>
|
||||
displayName: string
|
||||
setDisplayName: Dispatch<SetStateAction<string>> | null
|
||||
setDisplayName?: Dispatch<SetStateAction<string>>
|
||||
isAdmin: boolean
|
||||
setIsAdmin: Dispatch<SetStateAction<boolean>> | null
|
||||
setIsAdmin?: Dispatch<SetStateAction<boolean>>
|
||||
mode: ModeType
|
||||
runTimes: RunTimeType[]
|
||||
logout: (() => void) | null
|
||||
logout?: () => void
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppContextProps>({
|
||||
checkingSession: false,
|
||||
loggedIn: false,
|
||||
setLoggedIn: null,
|
||||
needsToUpdatePassword: false,
|
||||
setNeedsToUpdatePassword: null,
|
||||
userId: 0,
|
||||
setUserId: null,
|
||||
userId: '',
|
||||
username: '',
|
||||
setUsername: null,
|
||||
displayName: '',
|
||||
setDisplayName: null,
|
||||
isAdmin: false,
|
||||
setIsAdmin: null,
|
||||
mode: ModeType.Server,
|
||||
runTimes: [],
|
||||
logout: null
|
||||
runTimes: []
|
||||
})
|
||||
|
||||
const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
@@ -64,7 +57,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(0)
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [username, setUsername] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
4
web/src/types/declaration.d.ts
vendored
Normal file
4
web/src/types/declaration.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
@@ -6,13 +6,13 @@ export const findExistingPermission = (
|
||||
) => {
|
||||
for (const permission of existingPermissions) {
|
||||
if (
|
||||
permission.user?.id === newPermission.principalId &&
|
||||
permission.user?.uid === newPermission.principalId &&
|
||||
hasSameCombination(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
|
||||
if (
|
||||
permission.group?.groupId === newPermission.principalId &&
|
||||
permission.group?.uid === newPermission.principalId &&
|
||||
hasSameCombination(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
@@ -27,13 +27,13 @@ export const findUpdatingPermission = (
|
||||
) => {
|
||||
for (const permission of existingPermissions) {
|
||||
if (
|
||||
permission.user?.id === newPermission.principalId &&
|
||||
permission.user?.uid === newPermission.principalId &&
|
||||
hasDifferentSetting(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
|
||||
if (
|
||||
permission.group?.groupId === newPermission.principalId &&
|
||||
permission.group?.uid === newPermission.principalId &&
|
||||
hasDifferentSetting(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
|
||||
3
web/src/utils/index.ts
Normal file
3
web/src/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './log'
|
||||
export * from './types'
|
||||
export * from './helper'
|
||||
133
web/src/utils/log.ts
Normal file
133
web/src/utils/log.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { SyntheticEvent } from 'react'
|
||||
import { LogInstance } from './'
|
||||
|
||||
export const parseErrorsAndWarnings = (log: string) => {
|
||||
const logLines = log.split('\n')
|
||||
const errorLines: LogInstance[] = []
|
||||
const warningLines: LogInstance[] = []
|
||||
|
||||
logLines.forEach((line: string, index: number) => {
|
||||
// INFO: check if content in element starts with ERROR
|
||||
if (/<.*>ERROR/gm.test(line)) {
|
||||
const errorLine = line.substring(line.indexOf('E'), line.length - 1)
|
||||
|
||||
errorLines.push({
|
||||
body: errorLine,
|
||||
line: index,
|
||||
type: 'error',
|
||||
id: errorLines.length
|
||||
})
|
||||
}
|
||||
|
||||
// INFO: check if line starts with ERROR
|
||||
else if (/^ERROR/gm.test(line)) {
|
||||
errorLines.push({
|
||||
body: line,
|
||||
line: index,
|
||||
type: 'error',
|
||||
id: errorLines.length
|
||||
})
|
||||
|
||||
logLines[index] =
|
||||
`<font id="error_${
|
||||
errorLines.length - 1
|
||||
}" style="color: red;" ref={scrollTo}>` +
|
||||
logLines[index] +
|
||||
'</font>'
|
||||
}
|
||||
|
||||
// INFO: check if content in element starts with WARNING
|
||||
else if (/<.*>WARNING/gm.test(line)) {
|
||||
const warningLine = line.substring(line.indexOf('W'), line.length - 1)
|
||||
|
||||
warningLines.push({
|
||||
body: warningLine,
|
||||
line: index,
|
||||
type: 'warning',
|
||||
id: warningLines.length
|
||||
})
|
||||
}
|
||||
|
||||
// INFO: check if line starts with WARNING
|
||||
else if (/^WARNING/gm.test(line)) {
|
||||
warningLines.push({
|
||||
body: line,
|
||||
line: index,
|
||||
type: 'warning',
|
||||
id: warningLines.length
|
||||
})
|
||||
|
||||
logLines[index] =
|
||||
`<font id="warning_${warningLines.length - 1}" style="color: green;">` +
|
||||
logLines[index] +
|
||||
'</font>'
|
||||
}
|
||||
})
|
||||
|
||||
return { errors: errorLines, warnings: warningLines, logLines }
|
||||
}
|
||||
|
||||
export const defaultChunkSize = 20000
|
||||
|
||||
export const isTheLastChunk = (
|
||||
lineCount: number,
|
||||
chunkNumber: number,
|
||||
chunkSize = defaultChunkSize
|
||||
) => {
|
||||
if (lineCount <= chunkSize) return true
|
||||
|
||||
const chunksNumber = Math.ceil(lineCount / chunkSize)
|
||||
|
||||
return chunkNumber === chunksNumber
|
||||
}
|
||||
|
||||
export const splitIntoChunks = (log: string, chunkSize = defaultChunkSize) => {
|
||||
if (!log) return []
|
||||
|
||||
const logLines: string[] = log.split(`\n`)
|
||||
|
||||
if (logLines.length <= chunkSize) return [log]
|
||||
|
||||
const chunks: string[] = []
|
||||
|
||||
while (logLines.length) {
|
||||
const chunk = logLines.splice(0, chunkSize)
|
||||
|
||||
chunks.push(chunk.join(`\n`))
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
export const clearErrorsAndWarningsHtmlWrapping = (log: string) =>
|
||||
log.replace(/^<font[^>]*>/gm, '').replace(/<\/font>/gm, '')
|
||||
|
||||
export const download = (evt: SyntheticEvent, log: string, fileName = '') => {
|
||||
evt.stopPropagation()
|
||||
|
||||
const padWithZero = (num: number) => (num < 9 ? `0${num}` : `${num}`)
|
||||
|
||||
const date = new Date()
|
||||
const datePrefix = [
|
||||
date.getFullYear(),
|
||||
padWithZero(date.getMonth() + 1),
|
||||
padWithZero(date.getDate()),
|
||||
padWithZero(date.getHours()),
|
||||
padWithZero(date.getMinutes()),
|
||||
padWithZero(date.getSeconds())
|
||||
].join('')
|
||||
|
||||
const file = new Blob([log])
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${datePrefix}${fileName}.log`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
export interface UserResponse {
|
||||
id: number
|
||||
uid: string
|
||||
username: string
|
||||
displayName: string
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export interface GroupResponse {
|
||||
groupId: number
|
||||
uid: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export interface GroupDetailsResponse extends GroupResponse {
|
||||
}
|
||||
|
||||
export interface PermissionResponse {
|
||||
permissionId: number
|
||||
uid: string
|
||||
path: string
|
||||
type: string
|
||||
setting: string
|
||||
@@ -30,7 +30,7 @@ export interface RegisterPermissionPayload {
|
||||
type: string
|
||||
setting: string
|
||||
principalType: string
|
||||
principalId: number
|
||||
principalId: string
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
@@ -39,3 +39,18 @@ export interface TreeNode {
|
||||
isFolder: boolean
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
|
||||
export interface LogInstance {
|
||||
body: string
|
||||
line: number
|
||||
type: 'error' | 'warning'
|
||||
id: number
|
||||
ref?: any
|
||||
}
|
||||
|
||||
export interface LogObject {
|
||||
body: string
|
||||
errors?: LogInstance[]
|
||||
warnings?: LogInstance[]
|
||||
linesCount: number
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -33,9 +33,23 @@ const config: Configuration = {
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: ['/node_modules/'],
|
||||
exclude: ['/node_modules/', /\.module\.css$/],
|
||||
use: ['style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.module\.css$/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[local]--[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
exclude: ['/node_modules/'],
|
||||
|
||||
Reference in New Issue
Block a user