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

Compare commits

..

562 Commits
v0.7.1 ... main

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Features

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

### Features

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

### Features

* **stp:** added trigger endpoint ([b0723f1](b0723f1444))
2024-10-29 14:11:35 +00:00
Yury Shkoda
a044176054 Merge pull request #375 from sasjs/issue-373-stp
Issue 373 stp
2024-10-29 17:08:38 +03:00
Yury
deee34f5fd chore(stp): removed query logic from trigger endpoint 2024-10-29 16:55:40 +03:00
Yury
b0723f1444 feat(stp): added trigger endpoint 2024-10-29 16:27:53 +03:00
Yury
e9519cb3c6 chore(code): used correct type 2024-10-29 16:20:27 +03:00
semantic-release-bot
0838b8112e chore(release): 0.36.0 [skip ci]
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)

### Features

* **code:** added code/trigger API endpoint ([ffcf193](ffcf193b87))
2024-10-29 10:32:01 +00:00
Yury Shkoda
441f8b7726 Merge pull request #374 from sasjs/issue-373
feat(code): added code/trigger API endpoint
2024-10-29 13:29:08 +03:00
Yury
049a7f4b80 chore(swagger): improved description 2024-10-29 12:02:26 +03:00
Yury
3053c68bdf chore(lint): fixed linting issues 2024-10-29 11:40:44 +03:00
Yury
76750e864d chore(lint): fixed lint issue 2024-10-29 11:30:05 +03:00
Yury
ffcf193b87 feat(code): added code/trigger API endpoint 2024-10-29 11:18:04 +03:00
semantic-release-bot
aa2a1cbe13 chore(release): 0.35.4 [skip ci]
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)

### Bug Fixes

* **api:** fixed env issue in MacOS executable ([73d965d](73d965daf5))
2024-01-15 13:21:15 +00:00
Yury Shkoda
6f2c53555c Merge pull request #372 from sasjs/issue-371
fix(api): fixed env issue in MacOS executable
2024-01-15 16:18:10 +03:00
Yury
73d965daf5 fix(api): fixed env issue in MacOS executable 2024-01-15 15:14:06 +03:00
semantic-release-bot
4f1763db67 chore(release): 0.35.3 [skip ci]
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)

### Bug Fixes

* enable embedded LFs in JS STP vars ([7e8cbbf](7e8cbbf377))
2023-11-07 20:48:28 +00:00
Allan Bowe
28222add04 Merge pull request #370 from sasjs/allanbowe-patch-1
fix: enable embedded LFs in JS STP vars
2023-11-07 20:43:16 +00:00
Allan
068edfd6a5 chore: lint fix 2023-11-07 20:39:05 +00:00
Allan Bowe
7e8cbbf377 fix: enable embedded LFs in JS STP vars 2023-11-07 15:51:32 +00:00
Allan Bowe
1fc1431442 chore: using GITHUB_TOKEN 2023-08-07 20:11:40 +01:00
semantic-release-bot
3387efbb9a chore(release): 0.35.2 [skip ci]
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)

### Bug Fixes

* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](9586dbb2d0))
2023-08-07 18:53:12 +00:00
Allan Bowe
e2996b495f Merge pull request #365 from sasjs/swagger-fix
fix: add _debug as optional query param in swagger apis for  stp/execute
2023-08-07 19:48:28 +01:00
Allan
41c627f93a chore: lint fix 2023-08-07 19:39:02 +01:00
Allan Bowe
49f5dc7555 Update swagger.yaml 2023-08-07 19:32:29 +01:00
Allan Bowe
f6e77f99a4 Update swagger.yaml 2023-08-07 19:31:20 +01:00
Allan Bowe
b57dfa429b Update stp.ts 2023-08-07 19:30:09 +01:00
9586dbb2d0 fix: add _debug as optional query param in swagger apis for GET stp/execute 2023-08-07 22:01:52 +05:00
semantic-release-bot
a4f78ab48d chore(release): 0.35.1 [skip ci]
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)

### Bug Fixes

* **log-separator:** log separator should always wrap log ([8940f4d](8940f4dc47))
2023-07-25 06:05:23 +00:00
Yury Shkoda
2f47a2213b Merge pull request #364 from sasjs/log-separator
fix(log-separator): log separator should always wrap log
2023-07-25 09:01:36 +03:00
Yury Shkoda
0f91395fbb lint: fixed linting issues 2023-07-24 18:36:08 +03:00
Yury Shkoda
167b14fed0 docs(log-separator): left comment 2023-07-24 18:29:20 +03:00
Yury Shkoda
8940f4dc47 fix(log-separator): log separator should always wrap log 2023-07-24 18:27:21 +03:00
semantic-release-bot
48c1ada1b6 chore(release): 0.35.0 [skip ci]
# [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](d2de9dc13e))
* **execute:** added atribute indicating stp api ([e78f87f](e78f87f5c0))
* **execute:** fixed adding print output ([9aaffce](9aaffce820))
* **execution:** removed empty webout from response ([6dd2f4f](6dd2f4f876))
* **webout:** fixed adding empty webout to response payload ([31df72a](31df72ad88))

### Features

* **editor:** parse print output in response payload ([eb42683](eb42683fff))
2023-05-03 09:34:56 +00:00
Allan Bowe
0532488b55 Merge pull request #360 from sasjs/issue-354
Support print destination natively
2023-05-03 10:31:06 +01:00
Yury Shkoda
d458b5bb81 chore: cleanup 2023-05-03 10:56:17 +03:00
Yury Shkoda
958ab9cad2 chore(execution): add includePrintOutput to ExecuteFileParams 2023-05-03 10:46:21 +03:00
Yury Shkoda
78ceed13e1 docs(code): updated execute endpoint info 2023-05-02 16:01:00 +03:00
Yury Shkoda
a17814fc90 chore(stp): removed redundant argument 2023-05-02 15:53:13 +03:00
Yury Shkoda
9aaffce820 fix(execute): fixed adding print output 2023-05-02 15:49:44 +03:00
Yury Shkoda
e78f87f5c0 fix(execute): added atribute indicating stp api 2023-05-02 15:18:05 +03:00
Yury Shkoda
bd1b58086d docs: left a comment regarding payload parts 2023-05-02 12:10:17 +03:00
Yury Shkoda
9f521634d9 chore(webout): added comment 2023-05-02 11:30:55 +03:00
Yury Shkoda
a696168443 Merge branch 'main' of github.com:sasjs/server into issue-354 2023-05-02 11:17:41 +03:00
Yury Shkoda
31df72ad88 fix(webout): fixed adding empty webout to response payload 2023-05-02 11:17:12 +03:00
semantic-release-bot
d2239f75c2 chore(release): 0.34.2 [skip ci]
## [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](dba53de646))
2023-05-01 15:32:32 +00:00
Allan Bowe
45428892cc Merge pull request #362 from sasjs/remove-mongoose-sequence
fix: use custom logic for handling sequence ids
2023-05-01 16:28:47 +01:00
ac27a9b894 chore: remove residue 2023-05-01 19:54:43 +05:00
dba53de646 fix: use custom logic for handling sequence ids 2023-05-01 19:28:51 +05:00
Yury Shkoda
eb42683fff feat(editor): parse print output in response payload 2023-05-01 08:18:49 +03:00
Yury Shkoda
d2de9dc13e fix(editor): fixed log/webout/print tabs 2023-05-01 07:28:23 +03:00
Yury Shkoda
6dd2f4f876 fix(execution): removed empty webout from response 2023-04-28 17:25:30 +03:00
Yury Shkoda
c0f38ba7c9 wip(print-output): added print output to response payload 2023-04-28 15:09:44 +03:00
semantic-release-bot
d2f011e8a9 chore(release): 0.34.1 [skip ci]
## [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](9c5acd6de3))
* **log:** fixed chunk collapsing ([64b156f](64b156f762))
2023-04-28 11:50:19 +00:00
Yury Shkoda
5215633e96 Merge pull request #358 from sasjs/css-issue-fix
Css issue fix
2023-04-28 14:46:12 +03:00
Yury Shkoda
64b156f762 fix(log): fixed chunk collapsing 2023-04-28 13:30:25 +03:00
Yury Shkoda
9c5acd6de3 fix(css): fixed css loading 2023-04-28 13:29:31 +03:00
semantic-release-bot
3e72384a63 chore(release): 0.34.0 [skip ci]
# [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](02e2b060f9))
* **log:** fixed default runtime ([e04300a](e04300ad2a))
* **log:** fixed parsing log for different runtime ([3b1e4a1](3b1e4a128b))
* **log:** fixed scrolling issue ([56a522c](56a522c07c))
* **log:** fixed single chunk display ([8254b78](8254b78955))
* **log:** fixed single chunk scrolling ([57b7f95](57b7f954a1))
* **log:** fixed switching runtime ([c7a7399](c7a73991a7))
* **log:** fixing switching from SAS to other runtime ([c72ecc7](c72ecc7e59))

### Features

* **log:** added download chunk and entire log ([a38a9f9](a38a9f9c3d))
* **log:** added logComponent and LogTabWithIcons ([3a887de](3a887dec55))
* **log:** added parseErrorsAndWarnings utility ([7c1c1e2](7c1c1e2410))
* **log:** added time to downloaded log name ([3848bb0](3848bb0add))
* **log:** put download log icon into log tab ([777b3a5](777b3a55be))
* **log:** split large log into chunks ([75f5a3c](75f5a3c0b3))
* **log:** use improved log for SAS run time only ([7b12591](7b12591595))
2023-04-28 09:33:41 +00:00
Allan Bowe
df5d40b445 Merge pull request #351 from sasjs/issue-346
Improve SAS log
2023-04-28 10:29:13 +01:00
semantic-release-bot
c44ec35b3d chore(release): 0.33.3 [skip ci]
## [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](6a520f5b26))
2023-04-27 18:01:26 +00:00
Allan Bowe
77fac663c5 Merge pull request #357 from sasjs/cosmosdb-issue
fix: use RateLimiterMemory instead of RateLimiterMongo
2023-04-27 18:56:53 +01:00
Yury Shkoda
3848bb0add feat(log): added time to downloaded log name 2023-04-27 18:53:45 +03:00
Yury Shkoda
56a522c07c fix(log): fixed scrolling issue 2023-04-27 17:53:45 +03:00
Yury Shkoda
87e9172cfc chore(log): used css module to declare classes 2023-04-27 17:52:57 +03:00
7df9588e66 chore: fixed specs 2023-04-27 16:26:43 +05:00
6a520f5b26 fix: use RateLimiterMemory instead of RateLimiterMongo 2023-04-27 15:06:24 +05:00
Yury Shkoda
777b3a55be feat(log): put download log icon into log tab 2023-04-26 16:10:04 +03:00
semantic-release-bot
70c3834022 chore(release): 0.33.2 [skip ci]
## [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](d49ea47bd7))
2023-04-24 21:13:55 +00:00
Allan Bowe
dbf6c7de08 Merge pull request #355 from sasjs/issue274
fix: removing print redirection pending full #274 fix
2023-04-24 21:59:41 +01:00
allan
d49ea47bd7 fix: removing print redirection pending full #274 fix 2023-04-24 21:58:13 +01:00
Yury Shkoda
a38a9f9c3d feat(log): added download chunk and entire log 2023-04-21 17:21:09 +03:00
semantic-release-bot
be4951d112 chore(release): 0.33.1 [skip ci]
## [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](b4436bad0d)), closes [#352](https://github.com/sasjs/server/issues/352)
2023-04-20 08:26:33 +00:00
Allan Bowe
c116b263d9 Merge pull request #353 from sasjs/issue352
fix: applying nologo only for sas.exe
2023-04-20 09:22:29 +01:00
allan
b4436bad0d fix: applying nologo only for sas.exe
Closes #352
2023-04-20 09:16:22 +01:00
Yury Shkoda
57b7f954a1 fix(log): fixed single chunk scrolling 2023-04-18 16:16:58 +03:00
Yury Shkoda
8254b78955 fix(log): fixed single chunk display 2023-04-18 15:46:53 +03:00
Yury Shkoda
75f5a3c0b3 feat(log): split large log into chunks 2023-04-18 11:42:10 +03:00
Yury Shkoda
c72ecc7e59 fix(log): fixing switching from SAS to other runtime 2023-04-11 16:52:36 +03:00
Yury Shkoda
e04300ad2a fix(log): fixed default runtime 2023-04-11 16:42:24 +03:00
Yury Shkoda
c7a73991a7 fix(log): fixed switching runtime 2023-04-11 16:10:52 +03:00
Yury Shkoda
02e2b060f9 fix(log): fixed checks for errors and warnings 2023-04-11 15:21:46 +03:00
Yury Shkoda
3b1e4a128b fix(log): fixed parsing log for different runtime 2023-04-11 14:45:38 +03:00
Yury Shkoda
7b12591595 feat(log): use improved log for SAS run time only 2023-04-11 14:18:42 +03:00
Yury Shkoda
3a887dec55 feat(log): added logComponent and LogTabWithIcons 2023-04-10 16:21:32 +03:00
Yury Shkoda
7c1c1e2410 feat(log): added parseErrorsAndWarnings utility 2023-04-10 15:45:54 +03:00
Yury Shkoda
15774eca34 chore(deps): added react-highlight 2023-04-10 15:40:27 +03:00
semantic-release-bot
5e325522f4 chore(release): 0.33.0 [skip ci]
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)

### Features

* option to reset admin password on startup ([eda8e56](eda8e56bb0))
2023-04-05 22:07:50 +00:00
Allan Bowe
e576fad8f4 Merge pull request #350 from sasjs/issue-348
feat: option to reset admin password on startup
2023-04-05 23:03:21 +01:00
eda8e56bb0 feat: option to reset admin password on startup 2023-04-05 23:05:38 +05:00
semantic-release-bot
bee4f215d2 chore(release): 0.32.0 [skip ci]
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)

### Features

* add an api endpoint for admin to get list of client ids ([6ffaa7e](6ffaa7e9e2))
2023-04-05 09:44:13 +00:00
Allan Bowe
100f138f98 Merge pull request #349 from sasjs/issue-347
feat: add an api endpoint for admin to get list of client ids
2023-04-05 10:39:01 +01:00
6ffaa7e9e2 feat: add an api endpoint for admin to get list of client ids 2023-04-04 23:57:01 +05:00
semantic-release-bot
a433786011 chore(release): 0.31.0 [skip ci]
# [0.31.0](https://github.com/sasjs/server/compare/v0.30.3...v0.31.0) (2023-03-30)

### Features

* prevent brute force attack by rate limiting login endpoint ([a82cabb](a82cabb001))
2023-03-30 15:34:12 +00:00
Allan Bowe
1adff9a783 Merge pull request #345 from sasjs/issue-344
feat: prevent brute force attack against authorization
2023-03-30 16:29:15 +01:00
1435e380be chore: put comments on top of example in readme and .env.example 2023-03-30 15:35:16 +05:00
e099f2e678 chore: put comments on top of example in readme and .env.example 2023-03-30 15:34:50 +05:00
ddd155ba01 chore: combine scattered errors into a single object 2023-03-30 14:58:54 +05:00
9936241815 chore: fix failing specs 2023-03-29 23:46:25 +05:00
570995e572 chore: quick fix 2023-03-29 23:22:32 +05:00
462829fd9a chore: remove unused function 2023-03-29 22:10:16 +05:00
c1c0554de2 chore: quick fix 2023-03-29 22:05:29 +05:00
bd3aff9a7b chore: move secondsToHms to @sasjs/utils 2023-03-29 20:10:55 +05:00
a1e255e0c7 chore: removed unused file 2023-03-29 15:39:05 +05:00
0dae034f17 chore: revert change in package.json 2023-03-29 15:35:40 +05:00
89048ce943 chore: move brute force protection logic to middleware and a singleton class 2023-03-29 15:33:32 +05:00
a82cabb001 feat: prevent brute force attack by rate limiting login endpoint 2023-03-28 21:43:10 +05:00
c4066d32a0 chore: npm audit fix 2023-03-27 16:23:54 +05:00
semantic-release-bot
6a44cd69d9 chore(release): 0.30.3 [skip ci]
## [0.30.3](https://github.com/sasjs/server/compare/v0.30.2...v0.30.3) (2023-03-07)

### Bug Fixes

* add location.pathname to location.origin conditionally ([edab51c](edab51c519))
2023-03-07 10:45:49 +00:00
Allan Bowe
e607115995 Merge pull request #343 from sasjs/quick-fix
fix: add location.pathname to location.origin conditionally
2023-03-07 10:42:07 +00:00
edab51c519 fix: add location.pathname to location.origin conditionally 2023-03-07 15:37:22 +05:00
semantic-release-bot
081cc3102c chore(release): 0.30.2 [skip ci]
## [0.30.2](https://github.com/sasjs/server/compare/v0.30.1...v0.30.2) (2023-03-07)

### Bug Fixes

* **web:** add path to base in launch program url ([2c31922](2c31922f58))
2023-03-07 09:40:13 +00:00
Allan Bowe
b19aa1eba4 Merge pull request #342 from sasjs/quick-fix
fix(web): add path to base in launch program url
2023-03-07 09:35:09 +00:00
2c31922f58 fix(web): add path to base in launch program url 2023-03-07 09:05:29 +05:00
semantic-release-bot
4d7a571a6e chore(release): 0.30.1 [skip ci]
## [0.30.1](https://github.com/sasjs/server/compare/v0.30.0...v0.30.1) (2023-03-01)

### Bug Fixes

* **web:** add proper base url in axios.defaults ([5e3ce8a](5e3ce8a98f))
2023-03-01 18:38:43 +00:00
Allan Bowe
a373a4eb5f Merge pull request #341 from sasjs/base-url
fix(web): add proper base url in axios.defaults
2023-03-01 18:34:55 +00:00
5e3ce8a98f fix(web): add proper base url in axios.defaults 2023-03-01 21:45:18 +05:00
semantic-release-bot
737b34567e chore(release): 0.30.0 [skip ci]
# [0.30.0](https://github.com/sasjs/server/compare/v0.29.0...v0.30.0) (2023-02-28)

### Bug Fixes

* lint + remove default settings ([3de59ac](3de59ac4f8))

### Features

* add new env config DB_TYPE ([158f044](158f044363))
2023-02-28 21:08:30 +00:00
Allan Bowe
6373442f83 Merge pull request #340 from sasjs/issue-339
feat: add new env config DB_TYPE
2023-02-28 21:04:25 +00:00
munja
3de59ac4f8 fix: lint + remove default settings 2023-02-28 21:01:39 +00:00
Allan Bowe
941988cd7c chore(docs): linking to official docs 2023-02-28 20:55:32 +00:00
158f044363 feat: add new env config DB_TYPE 2023-03-01 01:41:08 +05:00
semantic-release-bot
02ae041a81 chore(release): 0.29.0 [skip ci]
# [0.29.0](https://github.com/sasjs/server/compare/v0.28.7...v0.29.0) (2023-02-06)

### Features

* Add /SASjsApi endpoint in permissions ([b3402ea](b3402ea80a))
2023-02-06 13:07:06 +00:00
Allan Bowe
c4c84b1537 Merge pull request #338 from sasjs/issue-224
feat: Add /SASjsApi endpoint in permissions
2023-02-06 13:02:49 +00:00
b3402ea80a feat: Add /SASjsApi endpoint in permissions 2023-02-06 15:29:24 +05:00
semantic-release-bot
abe942e697 chore(release): 0.28.7 [skip ci]
## [0.28.7](https://github.com/sasjs/server/compare/v0.28.6...v0.28.7) (2023-02-03)

### Bug Fixes

* add user to all users group on user creation ([2bae52e](2bae52e307))
2023-02-03 13:48:40 +00:00
Allan Bowe
faf2edb111 Merge pull request #337 from sasjs/issue-336
fix: add user to all users group on user creation
2023-02-03 13:44:46 +00:00
5bec453e89 chore: quick fix 2023-02-03 18:39:35 +05:00
7f2174dd2c chore: quick fix 2023-02-03 16:48:18 +05:00
2bae52e307 fix: add user to all users group on user creation 2023-02-03 16:47:18 +05:00
semantic-release-bot
b243e62ece chore(release): 0.28.6 [skip ci]
## [0.28.6](https://github.com/sasjs/server/compare/v0.28.5...v0.28.6) (2023-01-26)

### Bug Fixes

* show loading spinner on login screen while request is in process ([69f2576](69f2576ee6))
2023-01-26 18:20:28 +00:00
Sabir Hassan
88c3056e97 Merge pull request #335 from sasjs/issue-330
fix: show loading spinner on login screen while request is in process
2023-01-26 23:16:25 +05:00
203303b659 chore: bump the version of mongodb-memory-server 2023-01-26 23:12:46 +05:00
835709bd36 chore: npm audit fix 2023-01-26 23:10:20 +05:00
69f2576ee6 fix: show loading spinner on login screen while request is in process 2023-01-26 22:27:43 +05:00
semantic-release-bot
305077f36e chore(release): 0.28.5 [skip ci]
## [0.28.5](https://github.com/sasjs/server/compare/v0.28.4...v0.28.5) (2023-01-01)

### Bug Fixes

* adding NOPRNGETLIST system option for faster startup ([96eca3a](96eca3a35d))
2023-01-01 16:55:09 +00:00
Allan Bowe
96eca3a35d fix: adding NOPRNGETLIST system option for faster startup 2023-01-01 16:49:48 +00:00
semantic-release-bot
0f5c815c25 chore(release): 0.28.4 [skip ci]
## [0.28.4](https://github.com/sasjs/server/compare/v0.28.3...v0.28.4) (2022-12-07)

### Bug Fixes

* replace main class with container class ([71c429b](71c429b093))
2022-12-07 16:08:32 +00:00
Allan Bowe
acccef1e99 Merge pull request #334 from sasjs/issue-332
fix: Studio Editor autocomplete invisible
2022-12-07 16:04:42 +00:00
abc34ea047 chore: npm audit fix 2022-12-07 20:26:31 +05:00
71c429b093 fix: replace main class with container class 2022-12-07 20:25:06 +05:00
semantic-release-bot
c126f2d5d9 chore(release): 0.28.3 [skip ci]
## [0.28.3](https://github.com/sasjs/server/compare/v0.28.2...v0.28.3) (2022-12-06)

### Bug Fixes

* stringify json file ([1192583](1192583843))
2022-12-06 14:17:01 +00:00
Allan Bowe
34dd95d16e Merge pull request #333 from sasjs/issue-331
fix: stringify json file
2022-12-06 14:11:37 +00:00
1192583843 fix: stringify json file 2022-12-06 18:55:01 +05:00
semantic-release-bot
518815acf1 chore(release): 0.28.2 [skip ci]
## [0.28.2](https://github.com/sasjs/server/compare/v0.28.1...v0.28.2) (2022-12-05)

### Bug Fixes

* execute child process asyncronously ([23c997b](23c997b3be))
* JS / Python / R session folders should be NEW folders, not existing SAS folders ([39ba995](39ba995355))
2022-12-05 16:25:38 +00:00
Allan Bowe
80b7e14ed5 Merge pull request #329 from sasjs/issue-326
fix: non sas programs shouldn't use sas session folder
2022-12-05 16:21:58 +00:00
23c997b3be fix: execute child process asyncronously 2022-12-01 23:27:40 +05:00
39ba995355 fix: JS / Python / R session folders should be NEW folders, not existing SAS folders 2022-12-01 23:26:30 +05:00
semantic-release-bot
0e081e024b chore(release): 0.28.1 [skip ci]
## [0.28.1](https://github.com/sasjs/server/compare/v0.28.0...v0.28.1) (2022-11-28)

### Bug Fixes

* update the content type header after the program has been executed ([4dcee4b](4dcee4b3c3))
2022-11-28 23:25:10 +00:00
Allan Bowe
6a84bd0387 Merge pull request #327 from sasjs/issue-325
fix: default response header fixed when debug is ON
2022-11-28 23:20:30 +00:00
98d177a691 chore: audit fix 2022-11-28 23:55:21 +05:00
4dcee4b3c3 fix: update the content type header after the program has been executed 2022-11-28 23:53:36 +05:00
semantic-release-bot
4ffc1ec6a9 chore(release): 0.28.0 [skip ci]
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)

### Bug Fixes

* update the response header of request to stp/execute routes ([112431a](112431a1b7))

### Features

* **api:** add the api endpoint for updating user password ([4581f32](4581f32534))
* ask for updated password on first login ([1d48f88](1d48f8856b))
* **web:** add the UI for updating user password ([8b8c43c](8b8c43c21b))
2022-11-28 17:43:05 +00:00
Allan Bowe
5a1d168e83 Merge pull request #324 from sasjs/issue-322
fix: update the response header of request to stp/execute routes
2022-11-28 17:38:05 +00:00
Allan Bowe
515c976685 Merge pull request #323 from sasjs/issue-222
feat: force user to change password on first login
2022-11-28 17:37:17 +00:00
112431a1b7 fix: update the response header of request to stp/execute routes 2022-11-27 21:57:26 +05:00
c26485afec chore: fix specs 2022-11-22 20:15:26 +05:00
1d48f8856b feat: ask for updated password on first login 2022-11-22 19:58:17 +05:00
68758aa616 chore: new password should be different to current password 2022-11-22 15:26:22 +05:00
8b8c43c21b feat(web): add the UI for updating user password 2022-11-22 00:03:25 +05:00
4581f32534 feat(api): add the api endpoint for updating user password 2022-11-22 00:02:59 +05:00
b47e74a7e1 chore: styles fix 2022-11-22 00:01:58 +05:00
b27d684145 chore: use process.logger instead of condole.log 2022-11-17 23:03:33 +05:00
semantic-release-bot
6b666d5554 chore(release): 0.27.0 [skip ci]
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)

### Features

* on startup add webout.sas file in sasautos folder ([200f6c5](200f6c596a))
2022-11-17 13:21:44 +00:00
Allan Bowe
b5f0911858 Merge pull request #321 from sasjs/issue-318
feat: on startup add webout.sas file in sasautos folder
2022-11-17 13:17:35 +00:00
b86ba5b8a3 chore: lint fix 2022-11-17 17:49:00 +05:00
200f6c596a feat: on startup add webout.sas file in sasautos folder 2022-11-17 17:03:23 +05:00
semantic-release-bot
1b7ccda6e9 chore(release): 0.26.2 [skip ci]
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)

### Bug Fixes

* comments ([7ae862c](7ae862c5ce))
2022-11-15 13:06:36 +00:00
Allan Bowe
532035d835 Merge pull request #317 from sasjs/docfix
fix: comments
2022-11-15 13:01:45 +00:00
Allan Bowe
7ae862c5ce fix: comments 2022-11-15 13:01:13 +00:00
semantic-release-bot
ab5858b8af chore(release): 0.26.1 [skip ci]
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)

### Bug Fixes

* change the expiration of access/refresh tokens from days to seconds ([bb05493](bb054938c5))
2022-11-15 12:31:03 +00:00
Allan Bowe
a39f5dd9f1 Merge pull request #316 from sasjs/access-token-expiration
fix: change the expiration of access/refresh tokens from days to seconds
2022-11-15 12:25:41 +00:00
Allan Bowe
3ea444756c Update Client.ts 2022-11-15 11:00:42 +00:00
Allan Bowe
96399ecbbe Update swagger.yaml 2022-11-15 10:54:52 +00:00
bb054938c5 fix: change the expiration of access/refresh tokens from days to seconds 2022-11-15 15:48:03 +05:00
semantic-release-bot
fb6a556630 chore(release): 0.26.0 [skip ci]
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)

### Bug Fixes

* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](acc25cbd68))

### Features

* make access token duration configurable when creating client/secret ([2413c05](2413c05fea))
* make refresh token duration configurable ([abd5c64](abd5c64b4a))
2022-11-13 14:04:03 +00:00
Allan Bowe
9dbd8e16bd Merge pull request #315 from sasjs/issue-307
feat: make access token duration configurable when creating client
2022-11-13 14:00:03 +00:00
fe07c41f5f chore: update header 2022-11-11 15:35:24 +05:00
acc25cbd68 fix(web): dispose monaco editor actions in return of useEffect 2022-11-11 15:27:12 +05:00
4ca61feda6 chore: npm audit fix 2022-11-10 21:05:41 +05:00
abd5c64b4a feat: make refresh token duration configurable 2022-11-10 21:02:20 +05:00
2413c05fea feat: make access token duration configurable when creating client/secret 2022-11-10 19:43:06 +05:00
semantic-release-bot
4c874c2c39 chore(release): 0.25.1 [skip ci]
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)

### Bug Fixes

* **web:** use mui treeView instead of custom implementation ([c51b504](c51b50428f))
2022-11-07 15:50:02 +00:00
Allan Bowe
d819d79bc9 Merge pull request #313 from sasjs/tree-view
fix(web): use mui treeView instead of custom implementation
2022-11-07 15:46:14 +00:00
c51b50428f fix(web): use mui treeView instead of custom implementation 2022-11-06 01:14:58 +05:00
semantic-release-bot
e10a0554f0 chore(release): 0.25.0 [skip ci]
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)

### Features

* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](1c9d167f86))
2022-11-02 15:24:25 +00:00
Allan Bowe
337e2eb2a0 Merge pull request #311 from sasjs/issue-310
feat: Enable DRIVE_LOCATION setting for deploying multiple instances
2022-11-02 15:19:54 +00:00
66f8e7840b chore: update readme 2022-11-02 20:18:28 +05:00
1c9d167f86 feat: Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server 2022-11-02 20:05:12 +05:00
semantic-release-bot
7e684b54a6 chore(release): 0.24.0 [skip ci]
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)

### Features

* cli mock testing ([6434123](6434123401))
* mocking sas9 responses with JS STP ([36be3a7](36be3a7d5e))
2022-10-28 10:05:48 +00:00
Sabir Hassan
aafda2922b Merge pull request #306 from sasjs/sas9-tests-mock-dynamic
feat: cli mock testing
2022-10-28 15:01:00 +05:00
418bf41e38 style: lint 2022-10-28 11:53:42 +02:00
81f0b03b09 chore: comments address 2022-10-28 11:53:25 +02:00
fe5ae44aab chore: typo 2022-10-17 18:32:58 +02:00
36be3a7d5e feat: mocking sas9 responses with JS STP 2022-10-17 18:31:08 +02:00
6434123401 feat: cli mock testing 2022-10-11 18:37:20 +02:00
semantic-release-bot
0a6b972c65 chore(release): 0.23.4 [skip ci]
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)

### Bug Fixes

* add action to editor ref for running code ([2412622](2412622367))
2022-10-11 15:26:38 +00:00
Allan Bowe
be11707042 Merge pull request #303 from sasjs/issue-301
fix: add action to editor ref for running code
2022-10-11 16:08:57 +01:00
2412622367 fix: add action to editor ref for running code 2022-10-10 16:51:46 +05:00
semantic-release-bot
de3a190a8d chore(release): 0.23.3 [skip ci]
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)

### Bug Fixes

* added domain for session cookies ([94072c3](94072c3d24))
2022-10-09 20:32:07 +00:00
Allan Bowe
d5daafc6ed Merge pull request #302 from sasjs/cookies-with-domain
fix: added domain for session cookies
2022-10-09 21:26:40 +01:00
Saad Jutt
b1a2677b8c chore: specified domain for cookie for csrf as well 2022-10-10 00:48:13 +05:00
Saad Jutt
94072c3d24 fix: added domain for session cookies 2022-10-09 22:08:01 +05:00
semantic-release-bot
b64c0c12da chore(release): 0.23.2 [skip ci]
## [0.23.2](https://github.com/sasjs/server/compare/v0.23.1...v0.23.2) (2022-10-06)

### Bug Fixes

* bump in correct place ([14731e8](14731e8824))
* bumping sasjs/score ([258cc35](258cc35f14))
* reverting commit ([fda0e0b](fda0e0b57d))
2022-10-06 12:41:15 +00:00
Allan Bowe
79bc7b0e28 Merge pull request #300 from sasjs/corebump
fix: bumping sasjs/score
2022-10-06 13:36:20 +01:00
Allan Bowe
fda0e0b57d fix: reverting commit 2022-10-06 12:35:59 +00:00
Allan Bowe
14731e8824 fix: bump in correct place 2022-10-06 12:34:48 +00:00
Allan Bowe
258cc35f14 fix: bumping sasjs/score 2022-10-06 12:32:13 +00:00
semantic-release-bot
2295a518f0 chore(release): 0.23.1 [skip ci]
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)

### Bug Fixes

* ldap issues ([4d64420](4d64420c45))
2022-10-04 16:54:37 +00:00
Allan Bowe
1e5d621817 Merge pull request #298 from sasjs/fix-ldap
fix: ldap issues
2022-10-03 18:49:53 +01:00
4d64420c45 fix: ldap issues
logic fixed for updating user created by external auth provider
remove internal from AuthProviderType
replace AUTH_MECHANISM with AUTH_PROVIDERS
2022-10-03 21:24:10 +05:00
semantic-release-bot
799339de30 chore(release): 0.23.0 [skip ci]
# [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)

### Features

* Enable SAS_PACKAGES in SASjs Server ([424f0fc](424f0fc1fa))
2022-10-03 15:13:11 +00:00
Allan Bowe
042ed41189 Merge pull request #297 from sasjs/issue-292
feat: Enable SAS_PACKAGES in SASjs Server
2022-10-03 16:08:30 +01:00
424f0fc1fa feat: Enable SAS_PACKAGES in SASjs Server 2022-10-03 19:43:02 +05:00
semantic-release-bot
deafebde05 chore(release): 0.22.1 [skip ci]
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)

### Bug Fixes

* spelling issues ([3bb0597](3bb05974d2))
2022-10-03 13:17:14 +00:00
Allan Bowe
b66dc86b01 Merge pull request #296 from sasjs/spellingz
fix: spelling issues
2022-10-03 14:11:55 +01:00
Allan Bowe
3bb05974d2 fix: spelling issues 2022-10-03 13:10:30 +00:00
semantic-release-bot
d1c1a59e91 chore(release): 0.22.0 [skip ci]
# [0.22.0](https://github.com/sasjs/server/compare/v0.21.7...v0.22.0) (2022-10-03)

### Bug Fixes

* do not throw error on deleting group when it is created by an external auth provider ([68f0c5c](68f0c5c588))
* no need to restrict api endpoints when ldap auth is applied ([a142660](a14266077d))
* remove authProvider attribute from user and group payload interface ([bbd7786](bbd7786c6c))

### Features

* implemented LDAP authentication ([f915c51](f915c51b07))
2022-10-03 12:13:18 +00:00
Allan Bowe
668aff83fd Merge pull request #293 from sasjs/ldap
feat: integratedLDAP authentication
2022-10-03 13:09:07 +01:00
3fc06b80fc chore: add specs 2022-10-01 16:08:29 +05:00
bbd7786c6c fix: remove authProvider attribute from user and group payload interface 2022-10-01 15:06:55 +05:00
68f0c5c588 fix: do not throw error on deleting group when it is created by an external auth provider 2022-10-01 14:52:36 +05:00
semantic-release-bot
69ddf313b8 chore(release): 0.21.7 [skip ci]
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)

### Bug Fixes

* csrf package is changed to pillarjs-csrf ([fe3e508](fe3e5088f8))
2022-09-30 21:44:16 +00:00
Saad Jutt
65e404cdbd Merge pull request #294 from sasjs/csrf-package-migration
fix: csrf package is changed to pillarjs-csrf
2022-10-01 02:39:06 +05:00
a14266077d fix: no need to restrict api endpoints when ldap auth is applied 2022-09-30 14:41:09 +05:00
Saad Jutt
fda6ad6356 chore(csrf): removed _csrf completely 2022-09-30 03:07:21 +05:00
Saad Jutt
fe3e5088f8 fix: csrf package is changed to pillarjs-csrf 2022-09-29 20:33:30 +05:00
f915c51b07 feat: implemented LDAP authentication 2022-09-29 18:41:28 +05:00
semantic-release-bot
375f924f45 chore(release): 0.21.6 [skip ci]
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)

### Bug Fixes

* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](40f95f9072))
2022-09-23 09:33:49 +00:00
Allan Bowe
72329e30ed Merge pull request #291 from sasjs/issue-290
fix: in getTokensFromDB handle the scenario when tokens are expired
2022-09-23 10:29:51 +01:00
40f95f9072 fix: in getTokensFromDB handle the scenario when tokens are expired 2022-09-23 09:35:30 +05:00
semantic-release-bot
58e8a869ef chore(release): 0.21.5 [skip ci]
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)

### Bug Fixes

* made files extensions case insensitive ([2496043](249604384e))
2022-09-22 15:50:53 +00:00
Allan Bowe
b558a3d01d Merge pull request #289 from sasjs/issue-288
fix: made files extensions case insensitive
2022-09-22 16:47:00 +01:00
249604384e fix: made files extensions case insensitive 2022-09-22 20:37:16 +05:00
semantic-release-bot
056a436e10 chore(release): 0.21.4 [skip ci]
## [0.21.4](https://github.com/sasjs/server/compare/v0.21.3...v0.21.4) (2022-09-21)

### Bug Fixes

* removing single quotes from _program value ([a0e7875](a0e7875ae6))
2022-09-21 20:02:09 +00:00
Allan Bowe
06d59c618c Merge pull request #287 from sasjs/varfix
fix: removing single quotes from _program value
2022-09-21 20:58:28 +01:00
Allan Bowe
a0e7875ae6 fix: removing single quotes from _program value 2022-09-21 19:57:32 +00:00
semantic-release-bot
24966e695a chore(release): 0.21.3 [skip ci]
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)

### Bug Fixes

* return same tokens if not expired ([330c020](330c020933))
2022-09-21 17:49:49 +00:00
Allan Bowe
5c40d8a342 Merge pull request #286 from sasjs/issue-279
fix: return same tokens if not expired
2022-09-21 18:46:07 +01:00
6f5566dabb chore: lint fix 2022-09-21 22:29:50 +05:00
d93470d183 chore: improve code 2022-09-21 22:27:27 +05:00
330c020933 fix: return same tokens if not expired 2022-09-21 22:12:03 +05:00
munja
a810f6c7cf chore(docs): updating swagger definitions 2022-09-21 11:08:12 +01:00
semantic-release-bot
5d6c6086b4 chore(release): 0.21.2 [skip ci]
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)

### Bug Fixes

* default content-type for sas programs should be text/plain ([9977c9d](9977c9d161))
* **studio:** inject program path to code before sending for execution ([edc2e2a](edc2e2a302))
2022-09-20 21:08:08 +00:00
Allan Bowe
0edcbdcefc Merge pull request #283 from sasjs/fix-default-content-type
fix: default content-type for sas programs should be text/plain
2022-09-20 22:04:27 +01:00
Allan Bowe
ea0222f218 Merge pull request #285 from sasjs/issue-280
fix(studio): inject program path to code before sending for execution
2022-09-20 22:04:16 +01:00
edc2e2a302 fix(studio): inject program path to code before sending for execution 2022-09-21 01:57:01 +05:00
Allan Bowe
efd2e1450e Merge pull request #284 from sasjs/apidocs
chore(docs): updating API docs
2022-09-20 12:25:12 +01:00
munja
1092a73c10 chore(docs): updating API docs 2022-09-20 12:20:50 +01:00
9977c9d161 fix: default content-type for sas programs should be text/plain 2022-09-20 02:32:22 +05:00
semantic-release-bot
5c0eff5197 chore(release): 0.21.1 [skip ci]
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)

### Bug Fixes

* SASJS_WEBOUT_HEADERS path for windows ([0749d65](0749d65173))
2022-09-19 18:58:46 +00:00
Allan Bowe
3bda991a58 Merge pull request #282 from sasjs/issue-281
fix: SASJS_WEBOUT_HEADERS path for windows
2022-09-19 19:54:13 +01:00
0327f7c6ec chore: no need to escapeWinSlash in _sasjs_webout_headers 2022-09-19 23:51:18 +05:00
92549402eb chore: use utility function escapeWinSlashes 2022-09-19 23:36:04 +05:00
semantic-release-bot
b88c911527 chore(release): 0.21.0 [skip ci]
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)

### Features

* sas9 mocker improved - public access denied scenario ([06d3b17](06d3b17154))
2022-09-19 12:54:27 +00:00
Saad Jutt
8b12f31060 Merge pull request #276 from sasjs/sas9-mock
SAS9 mocker improved - public access denied scenario
2022-09-19 17:50:45 +05:00
Saad Jutt
e65cba9af0 chore: removed deprecated body-parser 2022-09-19 17:47:29 +05:00
0749d65173 fix: SASJS_WEBOUT_HEADERS path for windows 2022-09-19 15:53:51 +05:00
semantic-release-bot
a9c9b734f5 chore(release): 0.20.0 [skip ci]
# [0.20.0](https://github.com/sasjs/server/compare/v0.19.0...v0.20.0) (2022-09-16)

### Features

* add support for R stored programs ([d6651bb](d6651bbdbe))
2022-09-16 11:55:57 +00:00
Saad Jutt
39da41c9f1 Merge pull request #277 from sasjs/r-integration
R integration
2022-09-16 16:51:06 +05:00
662b2ca36a chore: replace env variable RSCRIPT_PATH with R_PATH 2022-09-09 15:23:46 +05:00
16b7aa6abb chore: merge js, py and r session controller classes to base session controller class 2022-09-09 00:49:26 +05:00
4560ef942f chore(web): refactor react code 2022-09-08 21:49:35 +05:00
06d3b17154 feat: sas9 mocker improved - public access denied scenario 2022-09-07 18:48:56 +02:00
d6651bbdbe feat: add support for R stored programs 2022-09-06 21:52:21 +05:00
b9d032f148 chore: update swagger.yaml 2022-09-06 21:51:17 +05:00
semantic-release-bot
70655e74d3 chore(release): 0.19.0 [skip ci]
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)

### Features

* added mocking endpoints ([0a0ba2c](0a0ba2cca5))
2022-09-05 12:21:34 +00:00
Allan Bowe
cb82fea0d8 Merge pull request #264 from sasjs/mocker
Mocker
2022-09-05 13:16:10 +01:00
b9a596616d chore: cleanup 2022-09-05 12:20:56 +02:00
semantic-release-bot
72a5393be3 chore(release): 0.18.0 [skip ci]
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)

### Features

* add option for program launch in context menu ([ee2db27](ee2db276bb))
2022-09-02 19:26:49 +00:00
Allan Bowe
769a840e9f Merge pull request #273 from sasjs/issue-270
feat: add option for program launch in context menu
2022-09-02 20:23:02 +01:00
730c7c52ac chore: remove commented code 2022-09-03 00:09:48 +05:00
ee2db276bb feat: add option for program launch in context menu 2022-09-02 23:40:02 +05:00
semantic-release-bot
d0a24aacb6 chore(release): 0.17.5 [skip ci]
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)

### Bug Fixes

* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](393b5eaf99))
2022-09-02 18:08:49 +00:00
Allan Bowe
57dfdf89a4 Merge pull request #272 from sasjs/allanbowe/session-crashed-since-271
fix: SASINITIALFOLDER split over 2 params, closes #271
2022-09-02 19:03:37 +01:00
Allan Bowe
393b5eaf99 fix: SASINITIALFOLDER split over 2 params, closes #271 2022-09-02 17:59:10 +00:00
Saad Jutt
7477326b22 chore: lower cased env values 2022-09-01 23:38:04 +05:00
Saad Jutt
76bf84316e chore: MOCK_SERVERTYPE instead of string literals 2022-09-01 23:34:57 +05:00
semantic-release-bot
e355276e44 chore(release): 0.17.4 [skip ci]
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)

### Bug Fixes

* invalid JS logic ([9f06080](9f06080348))
2022-09-01 12:50:14 +00:00
Allan Bowe
a3a9e3bd9f Merge pull request #269 from sasjs/allanbowe/error-unrecognized-sas-267
fix: invalid JS logic
2022-09-01 13:41:52 +01:00
Allan Bowe
9f06080348 fix: invalid JS logic 2022-09-01 12:35:58 +00:00
semantic-release-bot
4bbf9cfdb3 chore(release): 0.17.3 [skip ci]
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)

### Bug Fixes

* making SASINITIALFOLDER option windows only.  Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](e63271a67a))
2022-09-01 12:25:33 +00:00
Allan Bowe
e8e71fcde9 Merge pull request #268 from sasjs/allanbowe/error-unrecognized-sas-267
fix: making SASINITIALFOLDER option windows only.  Closes #267
2022-09-01 13:21:25 +01:00
Allan Bowe
e63271a67a fix: making SASINITIALFOLDER option windows only. Closes #267 2022-09-01 12:18:53 +00:00
7633608318 chore: mocker architecture fix, env validation 2022-08-31 13:31:28 +02:00
semantic-release-bot
e67d27d264 chore(release): 0.17.2 [skip ci]
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)

### Bug Fixes

* addition of SASINITIALFOLDER startup option.  Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](a5ee2f2923))
2022-08-31 09:35:51 +00:00
Allan Bowe
53033ccc96 Merge pull request #262 from sasjs/allanbowe/sas-default-folder-should-260
fix: addition of SASINITIALFOLDER startup option.  Closes #260
2022-08-31 10:32:14 +01:00
semantic-release-bot
6131ed1cbe chore(release): 0.17.1 [skip ci]
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)

### Bug Fixes

* typo mistake ([ee17d37](ee17d37aa1))
2022-08-30 18:15:33 +00:00
Allan Bowe
5d624e3399 Merge pull request #266 from sasjs/issue-265
fix: typo mistake
2022-08-30 19:10:47 +01:00
ee17d37aa1 fix: typo mistake 2022-08-30 22:42:11 +05:00
572fe22d50 chore: mocksas9 controller 2022-08-30 17:27:37 +02:00
091268bf58 chore: mocking only mandatory bits from sas9 responses 2022-08-29 12:40:29 +02:00
71a4a48443 chore: generic sas9 mock responses 2022-08-29 10:30:01 +02:00
3b188cd724 style: lint 2022-08-26 18:03:28 +02:00
eeba2328c0 chore: added login, logout endpoints 2022-08-26 17:59:07 +02:00
0a0ba2cca5 feat: added mocking endpoints 2022-08-25 15:58:08 +02:00
semantic-release-bot
476f834a80 chore(release): 0.17.0 [skip ci]
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)

### Bug Fixes

* allow underscores in file name ([bce83cb](bce83cb6fb))

### Features

* add the functionality of saving file by ctrl + s in editor ([3a3c90d](3a3c90d9e6))
2022-08-25 09:47:48 +00:00
Sabir Hassan
8b8739a873 Merge pull request #263 from sasjs/ctrl-save
feat: add the functionality of saving file by ctrl + s in editor
2022-08-25 14:43:50 +05:00
bce83cb6fb fix: allow underscores in file name 2022-08-25 14:27:42 +05:00
3a3c90d9e6 feat: add the functionality of saving file by ctrl + s in editor 2022-08-25 14:12:51 +05:00
semantic-release-bot
e63eaa5302 chore(release): 0.16.1 [skip ci]
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)

### Bug Fixes

* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](98ea2ac9b9))
2022-08-24 16:07:11 +00:00
Sabir Hassan
65de1bb175 Merge pull request #261 from sasjs/issue-258
fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute
2022-08-24 21:02:57 +05:00
Allan Bowe
a5ee2f2923 fix: addition of SASINITIALFOLDER startup option. Closes #260 2022-08-19 15:20:36 +00:00
98ea2ac9b9 fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute 2022-08-19 15:06:39 +05:00
semantic-release-bot
e94c56b23f chore(release): 0.16.0 [skip ci]
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)

### Bug Fixes

* add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py ([882bedd](882bedd5d5))
* update content for code.sas file ([02e88ae](02e88ae728))
* update default content type for python and js runtimes ([8780b80](8780b800a3))

### Features

* implement the logic for running python stored programs ([b06993a](b06993ab9e))
2022-08-17 20:49:53 +00:00
Allan Bowe
64f80e958d Merge pull request #259 from sasjs/python-runtime
feat: implement the logic for running python stored programs
2022-08-17 21:45:55 +01:00
bd97363c13 chore: quick fixes 2022-08-18 01:39:03 +05:00
02e88ae728 fix: update content for code.sas file 2022-08-18 01:20:33 +05:00
882bedd5d5 fix: add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py 2022-08-18 01:19:47 +05:00
8780b800a3 fix: update default content type for python and js runtimes 2022-08-18 01:17:17 +05:00
4c11082796 chore: addressed comments 2022-08-17 21:24:30 +05:00
a9b25b8880 chore: added specs for stp 2022-08-16 22:39:15 +05:00
b06993ab9e feat: implement the logic for running python stored programs 2022-08-16 15:51:37 +05:00
semantic-release-bot
f736e67517 chore(release): 0.15.3 [skip ci]
## [0.15.3](https://github.com/sasjs/server/compare/v0.15.2...v0.15.3) (2022-08-11)

### Bug Fixes

* adding proc printto in precode to enable print output in log.  Closes [#253](https://github.com/sasjs/server/issues/253) ([f8bb732](f8bb7327a8))
2022-08-11 15:07:31 +00:00
Allan Bowe
0f4a60c0c7 Merge pull request #254 from sasjs/allanbowe/sasjs-server-does-not-253
fix: adding proc printto in precode to enable print output in log.  Closes #253
2022-08-11 16:03:13 +01:00
Allan Bowe
f8bb7327a8 fix: adding proc printto in precode to enable print output in log. Closes #253 2022-08-11 15:01:46 +00:00
semantic-release-bot
abce135da2 chore(release): 0.15.2 [skip ci]
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)

### Bug Fixes

* remove vulnerabitities ([f27ac51](f27ac51fc4))
2022-08-10 11:28:07 +00:00
Allan Bowe
a6c014946a Merge pull request #252 from sasjs/fix-vulnerabilities
fix: remove vulnerabitities
2022-08-10 12:23:23 +01:00
f27ac51fc4 fix: remove vulnerabitities 2022-08-10 16:10:37 +05:00
semantic-release-bot
cb5be1be21 chore(release): 0.15.1 [skip ci]
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)

### Bug Fixes

* **web:** fix UI responsiveness ([d99fdd1](d99fdd1ec7))
2022-08-10 10:34:36 +00:00
Allan Bowe
d90fa9e5dd Merge pull request #251 from sasjs/issue-250
fix(web): fix UI responsiveness
2022-08-10 11:29:41 +01:00
d99fdd1ec7 fix(web): fix UI responsiveness 2022-08-10 15:18:05 +05:00
semantic-release-bot
399b5edad0 chore(release): 0.15.0 [skip ci]
# [0.15.0](https://github.com/sasjs/server/compare/v0.14.1...v0.15.0) (2022-08-05)

### Bug Fixes

* after selecting file in sidebar collapse sidebar in mobile view ([e215958](e215958b8b))
* improve mobile view for studio page ([c67d3ee](c67d3ee2f1))
* improve responsiveness for mobile view ([6ef40b9](6ef40b954a))
* improve user experience for adding permissions ([7a162ed](7a162eda8f))
* show logout button only when user is logged in ([9227cd4](9227cd449d))

### Features

* add multiple permission for same combination of type and principal at once ([754704b](754704bca8))
2022-08-05 09:59:19 +00:00
Allan Bowe
1dbc12e96b Merge pull request #249 from sasjs/issue-225
feat: add multiple permission for same combination of type and principal at once
2022-08-05 10:55:32 +01:00
e215958b8b fix: after selecting file in sidebar collapse sidebar in mobile view 2022-08-05 14:18:59 +05:00
9227cd449d fix: show logout button only when user is logged in 2022-08-05 01:22:27 +05:00
c67d3ee2f1 fix: improve mobile view for studio page 2022-08-05 01:10:15 +05:00
6ef40b954a fix: improve responsiveness for mobile view 2022-08-04 22:57:21 +05:00
semantic-release-bot
0d913baff1 chore(release): 0.14.1 [skip ci]
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)

### Bug Fixes

* **apps:** App Stream logo fix ([87c03c5](87c03c5f8d))
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](77f8d30baf))
* **env:** check added for not providing WHITELIST ([5966016](5966016853))
* **web:** show login on logged-out state ([f7fcc77](f7fcc7741a))
2022-08-04 12:10:31 +00:00
Allan Bowe
3671736c3d Merge pull request #248 from sasjs/cookies-management
fix(cookie): XSRF cookie is removed and passed token in head section
2022-08-04 13:06:30 +01:00
34cd84d8a9 chore: improve interface for add permission response 2022-08-04 16:34:15 +05:00
Saad Jutt
f7fcc7741a fix(web): show login on logged-out state 2022-08-04 05:39:28 +05:00
Saad Jutt
18052fdbf6 test: fixed failed specs 2022-08-04 04:01:51 +05:00
Saad Jutt
5966016853 fix(env): check added for not providing WHITELIST 2022-08-04 03:32:04 +05:00
Saad Jutt
87c03c5f8d fix(apps): App Stream logo fix 2022-08-04 03:03:27 +05:00
7a162eda8f fix: improve user experience for adding permissions 2022-08-04 02:51:59 +05:00
754704bca8 feat: add multiple permission for same combination of type and principal at once 2022-08-03 23:26:31 +05:00
Saad Jutt
77f8d30baf fix(cookie): XSRF cookie is removed and passed token in head section 2022-08-03 03:38:11 +05:00
semantic-release-bot
78bea7c154 chore(release): 0.14.0 [skip ci]
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)

### Bug Fixes

* add restriction on  add/remove user to public group ([d3a516c](d3a516c36e))
* call jwt.verify in synchronous way ([254bc07](254bc07da7))

### Features

* add public group to DB on seed ([c3e3bef](c3e3befc17))
* bypass authentication when route is enabled for public group ([68515f9](68515f95a6))
2022-08-02 19:08:38 +00:00
Saad Jutt
9c3b155c12 Merge pull request #246 from sasjs/issue-240
feat: bypass authentication when route is enabled for public group
2022-08-03 00:03:43 +05:00
Allan Bowe
98e501334f Update seedDB.ts 2022-08-02 19:33:16 +01:00
Allan Bowe
bbfd53e79e Update group.spec.ts 2022-08-02 19:32:44 +01:00
254bc07da7 fix: call jwt.verify in synchronous way 2022-08-02 23:05:42 +05:00
f978814ca7 chore: code refactor 2022-08-02 22:16:41 +05:00
68515f95a6 feat: bypass authentication when route is enabled for public group 2022-08-02 18:06:33 +05:00
d3a516c36e fix: add restriction on add/remove user to public group 2022-08-02 18:05:28 +05:00
c3e3befc17 feat: add public group to DB on seed 2022-08-02 18:04:00 +05:00
semantic-release-bot
275de9478e chore(release): 0.13.3 [skip ci]
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)

### Bug Fixes

* show non-admin user his own permissions only ([8a3054e](8a3054e19a))
* update schema of Permission ([5d5a9d3](5d5a9d3788))
2022-08-02 12:01:53 +00:00
Allan Bowe
1a3ef62cb2 Merge pull request #243 from sasjs/issue-241
fix: show non-admin user his own permissions only
2022-08-02 12:57:57 +01:00
semantic-release-bot
9eb5f3ca4d chore(release): 0.13.2 [skip ci]
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)

### Bug Fixes

* adding ls=max to reduce log size and improve readability ([916947d](916947dffa))
2022-08-01 22:42:31 +00:00
Allan Bowe
916947dffa fix: adding ls=max to reduce log size and improve readability 2022-08-01 22:38:31 +00:00
79b7827b7c chore: update tabs label in setting page 2022-08-01 23:01:05 +05:00
37e1aa9b61 chore: spec fixed 2022-08-01 22:54:31 +05:00
7e504008b7 chore: quick fix 2022-08-01 22:50:18 +05:00
5d5a9d3788 fix: update schema of Permission 2022-08-01 21:33:10 +05:00
semantic-release-bot
7c79d6479c chore(release): 0.13.1 [skip ci]
## [0.13.1](https://github.com/sasjs/server/compare/v0.13.0...v0.13.1) (2022-07-31)

### Bug Fixes

* adding options to prevent unwanted windows on windows.  Closes [#244](https://github.com/sasjs/server/issues/244) ([77db14c](77db14c690))
2022-07-31 17:09:11 +00:00
Allan Bowe
3e635f422a Merge pull request #245 from sasjs/allanbowe/avoid-batch-sas-window-244
fix: adding options to prevent unwanted windows on windows.  Closes #244
2022-07-31 18:05:05 +01:00
Allan Bowe
77db14c690 fix: adding options to prevent unwanted windows on windows. Closes #244 2022-07-31 16:58:33 +00:00
b7dff341f0 chore: fix specs 2022-07-30 00:18:02 +05:00
8a3054e19a fix: show non-admin user his own permissions only 2022-07-30 00:01:15 +05:00
semantic-release-bot
a531de2adb chore(release): 0.13.0 [skip ci]
# [0.13.0](https://github.com/sasjs/server/compare/v0.12.1...v0.13.0) (2022-07-28)

### Bug Fixes

* autofocus input field and submit on enter ([7681722](7681722e5a))
* move api button to user menu ([8de032b](8de032b543))

### Features

* add action and command to editor ([706e228](706e228a8e))
2022-07-28 19:27:12 +00:00
Allan Bowe
c458d94493 Merge pull request #239 from sasjs/issue-238
fix: improve user experience in the studio
2022-07-28 20:21:48 +01:00
706e228a8e feat: add action and command to editor 2022-07-28 23:56:44 +05:00
7681722e5a fix: autofocus input field and submit on enter 2022-07-28 23:55:59 +05:00
8de032b543 fix: move api button to user menu 2022-07-28 23:54:40 +05:00
semantic-release-bot
998ef213e9 chore(release): 0.12.1 [skip ci]
## [0.12.1](https://github.com/sasjs/server/compare/v0.12.0...v0.12.1) (2022-07-26)

### Bug Fixes

* **web:** disable launch icon button when file content is not saved ([c574b42](c574b42235))
* **web:** saveAs functionality fixed in studio page ([3c987c6](3c987c61dd))
* **web:** show original name as default name in rename file/folder modal ([9640f65](9640f65264))
* **web:** webout tab item fixed in studio page ([7cdffe3](7cdffe30e3))
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](3b1fcb937d))
2022-07-26 20:52:05 +00:00
Allan Bowe
f8b0f98678 Merge pull request #236 from sasjs/fix-studio
fix: issues fixed in studio page
2022-07-26 21:48:20 +01:00
9640f65264 fix(web): show original name as default name in rename file/folder modal 2022-07-27 01:44:13 +05:00
c574b42235 fix(web): disable launch icon button when file content is not saved 2022-07-27 01:42:46 +05:00
468d1a929d chore(web): quick fixes 2022-07-27 00:47:38 +05:00
7cdffe30e3 fix(web): webout tab item fixed in studio page 2022-07-26 23:53:07 +05:00
3b1fcb937d fix(web): when no file is selected save the editor content to local storage 2022-07-26 23:30:41 +05:00
3c987c61dd fix(web): saveAs functionality fixed in studio page 2022-07-26 23:15:42 +05:00
0a780697da chore(web): move hooks to hooks folder 2022-07-26 23:14:29 +05:00
83d819df53 chore(web): created custom useStateWithCallback hook 2022-07-26 23:12:55 +05:00
semantic-release-bot
95df2b21d6 chore(release): 0.12.0 [skip ci]
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)

### Bug Fixes

* fileTree api response to include an additional attribute isFolder ([0f19384](0f19384999))
* remove drive component ([06d7c91](06d7c91fc3))

### Features

* add api end point for delete folder ([08e0c61](08e0c61e0f))
* add sidebar(drive) to left of studio ([6c35412](6c35412d2f))
* created api endpoint for adding empty folder in drive ([941917e](941917e508))
* implemented api for renaming file/folder ([fdcaba9](fdcaba9d56))
* implemented delete file/folder functionality ([177675b](177675bc89))
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](0ce94a553e))
* implemented the functionality for renaming file/folder from context menu ([7010a6a](7010a6a120))
* prevent user from leaving studio page when there are unsaved changes ([6c75502](6c7550286b))
* **web:** add difference view editor in studio ([420a61a](420a61a5a6))
2022-07-26 14:29:41 +00:00
Allan Bowe
accdf914f1 Merge pull request #235 from sasjs/issue-198
feat: deprecate drive and add sidebar for file navigation to studio
2022-07-26 15:25:26 +01:00
15bdd2d7f0 chore: close file menu after clicking on diff editor menu item 2022-07-26 14:54:06 +05:00
2ce947d216 chore: code fixes 2022-07-26 14:16:27 +05:00
ce2114e3f6 chore: code fixes 2022-07-26 00:07:11 +05:00
6c7550286b feat: prevent user from leaving studio page when there are unsaved changes 2022-07-25 22:41:05 +05:00
2360e104bd chore: reduce the padding between tree items 2022-07-25 15:11:02 +05:00
420a61a5a6 feat(web): add difference view editor in studio 2022-07-25 15:01:04 +05:00
04e0f9efe3 chore: merge main into issue-198 2022-07-22 22:31:32 +05:00
99172cd9ed chore: add specs 2022-07-22 22:18:03 +05:00
57daad0c26 chore: error response codes for drive api 2022-07-22 16:58:26 +05:00
cc1e4543fc chore: add specs 2022-07-21 23:03:56 +05:00
03cb89d14f chore: code fixes 2022-07-21 23:03:40 +05:00
72140d73c2 chore: modified folderParamValidation method 2022-07-21 14:08:44 +05:00
efcefd2a42 chore: quick fix 2022-07-21 13:25:46 +05:00
06d7c91fc3 fix: remove drive component 2022-07-20 23:53:42 +05:00
7010a6a120 feat: implemented the functionality for renaming file/folder from context menu 2022-07-20 23:46:39 +05:00
fdcaba9d56 feat: implemented api for renaming file/folder 2022-07-20 23:45:11 +05:00
48688a6547 chore: update swagger docs 2022-07-20 16:52:49 +05:00
0ce94a553e feat: implemented functionality for adding file/folder from sidebar context menu 2022-07-20 16:45:45 +05:00
941917e508 feat: created api endpoint for adding empty folder in drive 2022-07-20 16:43:43 +05:00
semantic-release-bot
5706371ffd chore(release): 0.11.5 [skip ci]
## [0.11.5](https://github.com/sasjs/server/compare/v0.11.4...v0.11.5) (2022-07-19)

### Bug Fixes

* Revert "fix(security): missing cookie flags are added" ([ce5218a](ce5218a227))
2022-07-19 23:03:43 +00:00
Saad Jutt
ce5218a227 fix: Revert "fix(security): missing cookie flags are added"
This reverts commit 526402fd73.
2022-07-20 03:58:25 +05:00
semantic-release-bot
8b62755f39 chore(release): 0.11.4 [skip ci]
## [0.11.4](https://github.com/sasjs/server/compare/v0.11.3...v0.11.4) (2022-07-19)

### Bug Fixes

* **security:** missing cookie flags are added ([526402f](526402fd73))
2022-07-19 21:06:05 +00:00
Allan Bowe
cb84c3ebbb Merge pull request #234 from sasjs/issue147
fix(security): missing cookie flags are added
2022-07-19 22:02:05 +01:00
Saad Jutt
526402fd73 fix(security): missing cookie flags are added 2022-07-20 01:40:31 +05:00
177675bc89 feat: implemented delete file/folder functionality 2022-07-19 22:49:34 +05:00
721165ff12 chore: add delete confirmation modal and use it in permission component 2022-07-19 22:48:22 +05:00
08e0c61e0f feat: add api end point for delete folder 2022-07-19 22:41:03 +05:00
semantic-release-bot
1b234eb2b1 chore(release): 0.11.3 [skip ci]
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)

### Bug Fixes

* filePath fix in code.js file for windows ([2995121](299512135d))
2022-07-19 14:50:19 +00:00
Allan Bowe
ef25eec11f Merge pull request #233 from sasjs/issue-227
fix: filePath fix in code.js file for windows
2022-07-19 15:46:18 +01:00
3e53f70928 chore: update swagger docs 2022-07-19 16:14:40 +05:00
0f19384999 fix: fileTree api response to include an additional attribute isFolder 2022-07-19 16:13:46 +05:00
63dd6813c0 chore: lint fix 2022-07-19 13:07:34 +05:00
299512135d fix: filePath fix in code.js file for windows 2022-07-19 13:00:33 +05:00
6c35412d2f feat: add sidebar(drive) to left of studio 2022-07-18 22:39:09 +05:00
27410bc32b chore: add file path input modal 2022-07-18 22:37:32 +05:00
849b2dd468 chore: add custom tree view component 2022-07-18 22:32:10 +05:00
semantic-release-bot
a1a182698e chore(release): 0.11.2 [skip ci]
## [0.11.2](https://github.com/sasjs/server/compare/v0.11.1...v0.11.2) (2022-07-18)

### Bug Fixes

* apply icon option only for sas.exe ([d2ddd8a](d2ddd8aaca))
2022-07-18 12:39:49 +00:00
Allan Bowe
4be692b24b Merge pull request #232 from sasjs/issue229
fix: apply icon option only for sas.exe
2022-07-18 13:34:21 +01:00
Allan Bowe
d2ddd8aaca fix: apply icon option only for sas.exe 2022-07-18 12:33:52 +00:00
semantic-release-bot
3a45e8f525 chore(release): 0.11.1 [skip ci]
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)

### Bug Fixes

* bank operator ([aa02741](aa027414ed))
* ensuring nosplash option only applies for sas.exe ([65e6de9](65e6de9663)), closes [#229](https://github.com/sasjs/server/issues/229)
2022-07-18 12:14:31 +00:00
Allan Bowe
c0e2f55a7b Merge pull request #231 from sasjs/issue229
fix: bank operator
2022-07-18 13:10:30 +01:00
Allan Bowe
aa027414ed fix: bank operator 2022-07-18 12:09:54 +00:00
Allan Bowe
8c4c52b1a9 Merge pull request #230 from sasjs/issue229
fix: ensuring nosplash option only applies for sas.exe
2022-07-18 12:58:15 +01:00
Allan Bowe
ff420434ae chore: removing line added automatically 2022-07-18 11:57:19 +00:00
Allan Bowe
65e6de9663 fix: ensuring nosplash option only applies for sas.exe
Closes #229
2022-07-18 11:55:35 +00:00
semantic-release-bot
2e53d43e11 chore(release): 0.11.0 [skip ci]
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)

### Bug Fixes

* **logs:** logs location is configurable ([e024a92](e024a92f16))

### Features

* **logs:** logs to file with rotating + code split into files ([92fda18](92fda183f3))
2022-07-16 21:58:08 +00:00
Allan Bowe
3795f748a7 Merge pull request #228 from sasjs/issue217
Issue217
2022-07-16 22:54:13 +01:00
Saad Jutt
e024a92f16 fix(logs): logs location is configurable 2022-07-16 05:07:00 +05:00
Saad Jutt
92fda183f3 feat(logs): logs to file with rotating + code split into files 2022-07-16 04:42:54 +05:00
Saad Jutt
6f2e6efd03 chore: fixed few vulnerabilites 2022-07-16 03:30:29 +05:00
30d7a65358 chore: fix breaking changes caused by react-router-dom update 2022-07-15 18:42:59 +05:00
5e930f14d2 chore: bump mui/icons-material and react-router-dom versions 2022-07-15 18:41:11 +05:00
9bc68b1cdc chore: update swagger docs 2022-07-15 18:40:02 +05:00
Allan Bowe
3b4e9d20d4 Create FUNDING.yml 2022-07-08 20:51:10 +01:00
semantic-release-bot
4a67d0c63a chore(release): 0.10.0 [skip ci]
# [0.10.0](https://github.com/sasjs/server/compare/v0.9.0...v0.10.0) (2022-07-06)

### Bug Fixes

* add authorize middleware for appStreams ([e54a09d](e54a09db19))
* add isAdmin attribute to return response of get session and login requests ([bdf63df](bdf63df1d9))
* add permission authorization middleware to only specific routes ([f3dfc70](f3dfc7083f))
* bumping core and running lint ([a2d1396](a2d1396057))
* controller fixed for deleting permission ([b5f595a](b5f595a25c))
* do not show admin users in add permission modal ([a75edba](a75edbaa32))
* export GroupResponse interface ([38a7db8](38a7db8514))
* move permission filter modal to separate file and icons for different actions ([d000f75](d000f7508f))
* principalId type changed to  number from any ([4fcc191](4fcc191ce9))
* remove clientId from principal types ([0781ddd](0781ddd64e))
* remove duplicates principals from permission filter modal ([5b319f9](5b319f9ad1))
* show loading spinner in studio while executing code ([496247d](496247d0b9))
* show permission component only in server mode ([f863b81](f863b81a7d))
* update permission model ([39fc908](39fc908de1))
* update permission response ([e516b77](e516b7716d))
* **web:** only admin should be able to add, update or delete permission ([be8635c](be8635ccc5))

### Features

* add api endpoint for deleting permission ([0171344](01713440a4))
* add api endpoint for updating permission setting ([540f54f](540f54fb77))
* add authorize middleware for validating permissions ([7d916ec](7d916ec3e9))
* add basic UI for settings and permissions ([5652325](5652325452))
* add documentation link under usename dropdown menu ([eeb63b3](eeb63b330c))
* add permission model ([6bea1f7](6bea1f7666))
* add UI for updating permission ([e8c21a4](e8c21a43b2))
* add validation for registering permission ([e5200c1](e5200c1000))
* add, remove and update permissions from web component ([97ecfdc](97ecfdc955))
* added get authorizedRoutes api endpoint ([b10e932](b10e932605))
* created modal for adding permission ([1413b18](1413b18508))
* defined register permission and get all permissions api endpoints ([1103ffe](1103ffe07b))
* update swagger docs ([797c2bc](797c2bcc39))
2022-07-06 12:31:43 +00:00
Allan Bowe
dea204e3c5 Merge pull request #221 from sasjs/improve-web-UI
fix: Improve web UI
2022-07-06 13:26:19 +01:00
Allan Bowe
5f9e83759c Merge pull request #215 from sasjs/descs
chore: update descriptions
2022-07-06 13:26:00 +01:00
Allan Bowe
fefe63deb1 Merge pull request #151 from sasjs/issue-139
feat: enable permissions
2022-07-06 13:25:27 +01:00
ddd179bbee chore: added specs for verifying permissions 2022-07-05 16:18:14 +05:00
a10b87930c chore: quick fix 2022-07-05 15:29:44 +05:00
496247d0b9 fix: show loading spinner in studio while executing code 2022-07-05 08:23:51 +05:00
eeb63b330c feat: add documentation link under usename dropdown menu 2022-07-05 08:23:36 +05:00
Saad Jutt
1108d3dd7b chore: quick fix 2022-07-05 05:30:13 +05:00
Saad Jutt
7edb47a4cb chore: build fix 2022-07-05 03:40:54 +05:00
Saad Jutt
451cb4f6dd chore: fixed specs 2022-07-05 03:26:37 +05:00
Saad Jutt
0b759a5594 chore: code fixes API + web 2022-07-05 02:34:33 +05:00
Saad Jutt
5338ffb211 chore: Merge branch 'main' into issue-139 2022-07-04 23:59:51 +05:00
e42fdd3575 chore: conditionally call authorize middleware from authenticateToken 2022-07-04 20:13:46 +05:00
b10e932605 feat: added get authorizedRoutes api endpoint 2022-07-04 19:14:06 +05:00
e54a09db19 fix: add authorize middleware for appStreams 2022-07-04 17:14:17 +05:00
4c35e04802 chore: add snackbar for showing success alert 2022-07-04 16:00:23 +05:00
b5f595a25c fix: controller fixed for deleting permission 2022-07-04 04:27:58 +05:00
semantic-release-bot
a131adbae7 chore(release): 0.9.0 [skip ci]
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)

### Features

* removed secrets from env variables ([9c3da56](9c3da56901))
2022-07-03 10:40:36 +00:00
Allan Bowe
a20c3b9719 Merge pull request #220 from sasjs/issue213
feat: removed secrets from env variables
2022-07-03 11:36:24 +01:00
Saad Jutt
eee3a7b084 chore: code refactor 2022-07-03 07:03:15 +05:00
Saad Jutt
9c3da56901 feat: removed secrets from env variables 2022-07-03 06:56:18 +05:00
Allan Bowe
7e6524d7e4 chore: removing badge 2022-07-02 15:12:27 +01:00
Allan Bowe
0ea2690616 adding matrix chat link 2022-07-02 13:10:20 +01:00
semantic-release-bot
b369759f0f chore(release): 0.8.3 [skip ci]
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)

### Bug Fixes

* **deploy:** extract first json from zip file ([e290751](e290751c87))
2022-07-02 10:01:26 +00:00
Allan Bowe
ac9a835c5a Merge pull request #219 from sasjs/issue211
fix(deploy): extract first json from zip file
2022-07-02 10:57:16 +01:00
Saad Jutt
e290751c87 fix(deploy): extract first json from zip file 2022-07-02 14:39:33 +05:00
e516b7716d fix: update permission response 2022-07-02 01:03:53 +05:00
f3dfc7083f fix: add permission authorization middleware to only specific routes 2022-07-01 16:50:24 +05:00
7d916ec3e9 feat: add authorize middleware for validating permissions 2022-06-29 23:06:58 +05:00
70f279a49c chore: update swagger.yaml 2022-06-28 09:23:53 +05:00
66a3537271 chore: add specs 2022-06-28 06:50:35 +05:00
ca64c13909 chore: add principal type and permission setting enums 2022-06-28 00:00:04 +05:00
0a73a35547 chore: improve error handling 2022-06-27 23:21:48 +05:00
a75edbaa32 fix: do not show admin users in add permission modal 2022-06-26 01:49:07 +05:00
4ddfec0403 chore: add isAdmin field in user response 2022-06-26 01:48:31 +05:00
35439d7d51 chore: throw error when adding permission for admin user 2022-06-24 23:19:19 +05:00
907aa485fd chore: throw error when creating duplicate permission 2022-06-24 23:15:41 +05:00
888627e1c8 chore: close filterModal after applying/reseting 2022-06-24 22:32:18 +05:00
9cb9e2dd33 chore: add filter based on principal type 2022-06-24 16:28:41 +05:00
54d4bf835d chore: show principal type in permissions list 2022-06-24 15:50:09 +05:00
67fe298fd5 chore: lint fixes 2022-06-24 14:55:05 +05:00
97ecfdc955 feat: add, remove and update permissions from web component 2022-06-24 14:48:57 +05:00
5b319f9ad1 fix: remove duplicates principals from permission filter modal 2022-06-23 23:58:40 +05:00
be8635ccc5 fix(web): only admin should be able to add, update or delete permission 2022-06-23 23:35:06 +05:00
f863b81a7d fix: show permission component only in server mode 2022-06-23 23:14:54 +05:00
bdf63df1d9 fix: add isAdmin attribute to return response of get session and login requests 2022-06-23 22:50:00 +05:00
4c6b9c5e93 Merge branch 'main' into issue-139 2022-06-23 17:21:52 +05:00
Allan Bowe
a2d1396057 fix: bumping core and running lint 2022-06-23 09:00:22 +00:00
Allan Bowe
b2f21eb3ac chore: update descriptions 2022-06-23 08:44:12 +00:00
semantic-release-bot
71bcbb9134 chore(release): 0.8.2 [skip ci]
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)

### Bug Fixes

* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](5cc85b57f8))
2022-06-22 10:18:59 +00:00
Allan Bowe
c86f0feff8 Merge pull request #214 from sasjs/fix-runtime-filePath
fix: getRuntimeAndFilePath function
2022-06-22 12:14:12 +02:00
Allan Bowe
d3d2ab9a36 Update getRunTimeAndFilePath.ts 2022-06-22 11:12:48 +01:00
5cc85b57f8 fix: getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes 2022-06-22 14:24:06 +05:00
semantic-release-bot
ae0fc0c48c chore(release): 0.8.1 [skip ci]
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)

### Bug Fixes

* make CA_ROOT optional in getCertificates method ([1b5859e](1b5859ee37))
* update /logout route to /SASLogon/logout ([65380be](65380be2f3))
2022-06-21 20:10:31 +00:00
Saad Jutt
555c5d54e2 Merge pull request #212 from sasjs/update-logout-route
Update logout route
2022-06-21 13:06:30 -07:00
1b5859ee37 fix: make CA_ROOT optional in getCertificates method 2022-06-22 00:25:41 +05:00
65380be2f3 fix: update /logout route to /SASLogon/logout 2022-06-22 00:24:41 +05:00
Yury Shkoda
1933be15c2 Merge pull request #210 from sasjs/pr-template
chore(template): added pull request template
2022-06-21 19:11:19 +03:00
Yury Shkoda
56b20beb8c chore(template): added pull request template 2022-06-21 19:07:14 +03:00
semantic-release-bot
bfc5ac6a4f chore(release): 0.8.0 [skip ci]
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)

### Features

* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](2119e9de9a))
2022-06-21 07:24:26 +00:00
Allan Bowe
6376173de0 Merge pull request #209 from sasjs/certificate-ca-root
feat(certs): ENV variables updated and set CA Root for HTTPS server
2022-06-21 09:18:15 +02:00
Saad Jutt
3130fbeff0 chore: code refactor for getting session controller 2022-06-21 03:41:44 +05:00
Saad Jutt
01e9a1d9e9 chore: README.md and .env.example updated 2022-06-21 03:26:12 +05:00
Saad Jutt
2119e9de9a feat(certs): ENV variables updated and set CA Root for HTTPS server 2022-06-21 03:17:14 +05:00
semantic-release-bot
87dbab98f6 chore(release): 0.7.3 [skip ci]
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)

### Bug Fixes

* path descriptions and defaults ([5d5d6ce](5d5d6ce326))
2022-06-20 15:31:20 +00:00
Allan Bowe
1bf122a0a2 Merge pull request #207 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: path descriptions and defaults
2022-06-20 17:26:56 +02:00
Allan Bowe
5d5d6ce326 fix: path descriptions and defaults 2022-06-20 15:07:17 +00:00
semantic-release-bot
620eddb713 chore(release): 0.7.2 [skip ci]
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)

### Bug Fixes

* removing UTF-8 options from commandline.  There appears to be no reliable way to enforce ([f6dc74f](f6dc74f16b))
2022-06-20 15:03:27 +00:00
Allan Bowe
3c92034da3 Merge pull request #206 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: removing UTF-8 options from commandline.
2022-06-20 16:57:58 +02:00
Allan Bowe
f6dc74f16b fix: removing UTF-8 options from commandline. There appears to be no reliable way to enforce
UTF-8 without additional modifications to the PATH variable to ensure a DBCS instance of SAS.
2022-06-20 14:38:19 +00:00
fa63dc071b chore: update specs and swagger.yaml 2022-05-18 00:29:42 +05:00
e8c21a43b2 feat: add UI for updating permission 2022-05-18 00:20:49 +05:00
1413b18508 feat: created modal for adding permission 2022-05-18 00:05:28 +05:00
dfbd155711 chore: move common interfaces to utils folder 2022-05-18 00:04:37 +05:00
4fcc191ce9 fix: principalId type changed to number from any 2022-05-18 00:03:11 +05:00
d000f7508f fix: move permission filter modal to separate file and icons for different actions 2022-05-17 15:42:29 +05:00
5652325452 feat: add basic UI for settings and permissions 2022-05-16 23:53:30 +05:00
0781ddd64e fix: remove clientId from principal types 2022-05-16 19:56:56 +05:00
7be77cc38a chore: remvoe code redundancy and add specs for get permissions api endpoint 2022-05-10 07:05:59 +05:00
98b8a75148 chore: add specs for delete permission api endpoint 2022-05-10 06:40:34 +05:00
72a3197a06 chore: add spec for update permission when permission with provided id not exists 2022-05-10 06:25:52 +05:00
fce05d6959 chore: add spec for invalid principal type 2022-05-10 06:18:19 +05:00
1aec3abd28 chore: add specs for update permission api endpoint 2022-05-10 06:11:24 +05:00
9136c95013 chore: write specs for create permission api endpoint 2022-05-09 13:08:15 +05:00
Saad Jutt
89b32e70ff refactor: code in permission controller 2022-04-30 03:49:26 +05:00
01713440a4 feat: add api endpoint for deleting permission 2022-04-30 01:16:52 +05:00
540f54fb77 feat: add api endpoint for updating permission setting 2022-04-30 01:02:47 +05:00
bf906aa544 Merge branch 'main' into issue-139 2022-04-29 15:41:35 +05:00
797c2bcc39 feat: update swagger docs 2022-04-29 15:31:24 +05:00
1103ffe07b feat: defined register permission and get all permissions api endpoints 2022-04-29 15:30:41 +05:00
e5200c1000 feat: add validation for registering permission 2022-04-29 15:28:29 +05:00
38a7db8514 fix: export GroupResponse interface 2022-04-29 15:27:34 +05:00
39fc908de1 fix: update permission model 2022-04-29 15:26:26 +05:00
be009d5b02 Merge branch 'main' into issue-139 2022-04-29 00:32:36 +05:00
6bea1f7666 feat: add permission model 2022-04-28 21:18:23 +05:00
192 changed files with 26469 additions and 25675 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [sasjs]

View File

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

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
@@ -56,4 +56,4 @@ jobs:
- name: Release
run: |
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release

View File

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

View File

@@ -1,3 +1,817 @@
## [0.39.3](https://github.com/sasjs/server/compare/v0.39.2...v0.39.3) (2025-11-25)
### Bug Fixes
* (deps) bump @sasjs/core to 4.59.7 ([ab96653](https://github.com/sasjs/server/commit/ab966535642d08d4e8e984007b98c8fdffbe30f7))
* (deps) rerun npm i to sync ([225f381](https://github.com/sasjs/server/commit/225f381bdf8ad5aa2af8d75648df1dd5175e12e0))
## [0.39.2](https://github.com/sasjs/server/compare/v0.39.1...v0.39.2) (2025-09-25)
### Bug Fixes
* addressing test fail ([e51b204](https://github.com/sasjs/server/commit/e51b20421adc1598ea267c79b1fb4dbc085f97b9))
* packages missmatch ([379ea60](https://github.com/sasjs/server/commit/379ea604bcb5686b5299fae6a32f759c45b275ea))
* type libs ([6d123c3](https://github.com/sasjs/server/commit/6d123c3e23628c1d703eaa13142c77f0da970a55))
* typescript errors ([631e956](https://github.com/sasjs/server/commit/631e95604b64b1a96f2abade659348618f3b00b2))
* typescript errors ([198cd79](https://github.com/sasjs/server/commit/198cd79354254511c21ac1acfbf7b6bcfdab2af7))
## [0.39.1](https://github.com/sasjs/server/compare/v0.39.0...v0.39.1) (2025-03-13)
### Bug Fixes
* extra bit of sleep for file recognition ([f4768bf](https://github.com/sasjs/server/commit/f4768bffd3dbb2fe243966572ba74002024d96e1)), closes [#381](https://github.com/sasjs/server/issues/381)
# [0.39.0](https://github.com/sasjs/server/compare/v0.38.0...v0.39.0) (2024-10-31)
### Bug Fixes
* **api:** fixed condition in processProgram ([48a9a4d](https://github.com/sasjs/server/commit/48a9a4dd0e31f84209635382be4ec4bb2c3a9c0c))
### Features
* **api:** added session state endpoint ([6b6546c](https://github.com/sasjs/server/commit/6b6546c7ad0833347f8dc4cdba6ad19132f7aaef))
# [0.38.0](https://github.com/sasjs/server/compare/v0.37.0...v0.38.0) (2024-10-30)
### Features
* **api:** enabled query params in stp/trigger endpoint ([5cda9cd](https://github.com/sasjs/server/commit/5cda9cd5d8623b7ea2ecd989d7808f47ec866672))
# [0.37.0](https://github.com/sasjs/server/compare/v0.36.0...v0.37.0) (2024-10-29)
### Features
* **stp:** added trigger endpoint ([b0723f1](https://github.com/sasjs/server/commit/b0723f14448d60ffce4f2175cf8a73fc4d4dd0ee))
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)
### 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)
### Features
* option to reset admin password on startup ([eda8e56](https://github.com/sasjs/server/commit/eda8e56bb0ea20fdaacabbbe7dcf1e3ea7bd215a))
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)
### Features
* add an api endpoint for admin to get list of client ids ([6ffaa7e](https://github.com/sasjs/server/commit/6ffaa7e9e2a62c083bb9fcc3398dcbed10cebdb1))
# [0.31.0](https://github.com/sasjs/server/compare/v0.30.3...v0.31.0) (2023-03-30)
### Features
* prevent brute force attack by rate limiting login endpoint ([a82cabb](https://github.com/sasjs/server/commit/a82cabb00134c79c5ee77afd1b1628a1f768e050))
## [0.30.3](https://github.com/sasjs/server/compare/v0.30.2...v0.30.3) (2023-03-07)
### Bug Fixes
* add location.pathname to location.origin conditionally ([edab51c](https://github.com/sasjs/server/commit/edab51c51997f17553e037dc7c2b5e5fa6ea8ffe))
## [0.30.2](https://github.com/sasjs/server/compare/v0.30.1...v0.30.2) (2023-03-07)
### Bug Fixes
* **web:** add path to base in launch program url ([2c31922](https://github.com/sasjs/server/commit/2c31922f58a8aa20d7fa6bfc95b53a350f90c798))
## [0.30.1](https://github.com/sasjs/server/compare/v0.30.0...v0.30.1) (2023-03-01)
### Bug Fixes
* **web:** add proper base url in axios.defaults ([5e3ce8a](https://github.com/sasjs/server/commit/5e3ce8a98f1825e14c1d26d8da0c9821beeff7b3))
# [0.30.0](https://github.com/sasjs/server/compare/v0.29.0...v0.30.0) (2023-02-28)
### Bug Fixes
* lint + remove default settings ([3de59ac](https://github.com/sasjs/server/commit/3de59ac4f8e3d95cad31f09e6963bd04c4811f26))
### Features
* add new env config DB_TYPE ([158f044](https://github.com/sasjs/server/commit/158f044363abf2576c8248f0ca9da4bc9cb7e9d8))
# [0.29.0](https://github.com/sasjs/server/compare/v0.28.7...v0.29.0) (2023-02-06)
### Features
* Add /SASjsApi endpoint in permissions ([b3402ea](https://github.com/sasjs/server/commit/b3402ea80afb8802eee8b8b6cbbbcc29903424bc))
## [0.28.7](https://github.com/sasjs/server/compare/v0.28.6...v0.28.7) (2023-02-03)
### Bug Fixes
* add user to all users group on user creation ([2bae52e](https://github.com/sasjs/server/commit/2bae52e307327d7ee4a94b19d843abdc0ccec9d1))
## [0.28.6](https://github.com/sasjs/server/compare/v0.28.5...v0.28.6) (2023-01-26)
### Bug Fixes
* show loading spinner on login screen while request is in process ([69f2576](https://github.com/sasjs/server/commit/69f2576ee6d3d7b7f3325922a88656d511e3ac88))
## [0.28.5](https://github.com/sasjs/server/compare/v0.28.4...v0.28.5) (2023-01-01)
### Bug Fixes
* adding NOPRNGETLIST system option for faster startup ([96eca3a](https://github.com/sasjs/server/commit/96eca3a35dce4521150257ee019beb4488c8a08f))
## [0.28.4](https://github.com/sasjs/server/compare/v0.28.3...v0.28.4) (2022-12-07)
### Bug Fixes
* replace main class with container class ([71c429b](https://github.com/sasjs/server/commit/71c429b093b91e2444ae75d946579dccc2e48636))
## [0.28.3](https://github.com/sasjs/server/compare/v0.28.2...v0.28.3) (2022-12-06)
### Bug Fixes
* stringify json file ([1192583](https://github.com/sasjs/server/commit/1192583843d7efd1a6ab6943207f394c3ae966be))
## [0.28.2](https://github.com/sasjs/server/compare/v0.28.1...v0.28.2) (2022-12-05)
### Bug Fixes
* execute child process asyncronously ([23c997b](https://github.com/sasjs/server/commit/23c997b3beabeb6b733ae893031d2f1a48f28ad2))
* JS / Python / R session folders should be NEW folders, not existing SAS folders ([39ba995](https://github.com/sasjs/server/commit/39ba995355daa24bb7ab22720f8fc57d2dc85f40))
## [0.28.1](https://github.com/sasjs/server/compare/v0.28.0...v0.28.1) (2022-11-28)
### Bug Fixes
* update the content type header after the program has been executed ([4dcee4b](https://github.com/sasjs/server/commit/4dcee4b3c3950d402220b8f451c50ad98a317d83))
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)
### Bug Fixes
* update the response header of request to stp/execute routes ([112431a](https://github.com/sasjs/server/commit/112431a1b7461989c04100418d67d975a2a8f354))
### Features
* **api:** add the api endpoint for updating user password ([4581f32](https://github.com/sasjs/server/commit/4581f325344eb68c5df5a28492f132312f15bb5c))
* ask for updated password on first login ([1d48f88](https://github.com/sasjs/server/commit/1d48f8856b1fbbf3ef868914558333190e04981f))
* **web:** add the UI for updating user password ([8b8c43c](https://github.com/sasjs/server/commit/8b8c43c21bde5379825c5ec44ecd81a92425f605))
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)
### Features
* on startup add webout.sas file in sasautos folder ([200f6c5](https://github.com/sasjs/server/commit/200f6c596a6e732d799ed408f1f0fd92f216ba58))
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)
### Bug Fixes
* comments ([7ae862c](https://github.com/sasjs/server/commit/7ae862c5ce720e9483d4728f4295dede4f849436))
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)
### Bug Fixes
* change the expiration of access/refresh tokens from days to seconds ([bb05493](https://github.com/sasjs/server/commit/bb054938c5bd0535ae6b9da93ba0b14f9b80ddcd))
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)
### Bug Fixes
* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))
### Features
* make access token duration configurable when creating client/secret ([2413c05](https://github.com/sasjs/server/commit/2413c05fea3960f7e5c3c8b7b2f85d61314f08db))
* make refresh token duration configurable ([abd5c64](https://github.com/sasjs/server/commit/abd5c64b4a726e3f17594a98111b6aa269b71fee))
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
### Bug Fixes
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
### Features
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
### Features
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
### Bug Fixes
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
### Bug Fixes
* added domain for session cookies ([94072c3](https://github.com/sasjs/server/commit/94072c3d24a4d0d4c97900dc31bfbf1c9d2559b7))
## [0.23.2](https://github.com/sasjs/server/compare/v0.23.1...v0.23.2) (2022-10-06)
### Bug Fixes
* bump in correct place ([14731e8](https://github.com/sasjs/server/commit/14731e8824fa9f3d1daf89fd62f9916d5e3fcae4))
* bumping sasjs/score ([258cc35](https://github.com/sasjs/server/commit/258cc35f14cf50f2160f607000c60de27593fd79))
* reverting commit ([fda0e0b](https://github.com/sasjs/server/commit/fda0e0b57d56e3b5231e626a8d933343ac0c5cdc))
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)
### Bug Fixes
* ldap issues ([4d64420](https://github.com/sasjs/server/commit/4d64420c45424134b4d2014a2d5dd6e846ed03b3))
# [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)
### Features
* Enable SAS_PACKAGES in SASjs Server ([424f0fc](https://github.com/sasjs/server/commit/424f0fc1faec765eb7a14619584e649454105b70))
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)
### Bug Fixes
* spelling issues ([3bb0597](https://github.com/sasjs/server/commit/3bb05974d216d69368f4498eb9f309bce7d97fd8))
# [0.22.0](https://github.com/sasjs/server/compare/v0.21.7...v0.22.0) (2022-10-03)
### Bug Fixes
* do not throw error on deleting group when it is created by an external auth provider ([68f0c5c](https://github.com/sasjs/server/commit/68f0c5c5884431e7e8f586dccf98132abebb193e))
* no need to restrict api endpoints when ldap auth is applied ([a142660](https://github.com/sasjs/server/commit/a14266077d3541c7a33b7635efa4208335e73519))
* remove authProvider attribute from user and group payload interface ([bbd7786](https://github.com/sasjs/server/commit/bbd7786c6ce13b374d896a45c23255b8fa3e8bd2))
### Features
* implemented LDAP authentication ([f915c51](https://github.com/sasjs/server/commit/f915c51b077a2b8c4099727355ed914ecd6364bd))
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)
### Bug Fixes
* csrf package is changed to pillarjs-csrf ([fe3e508](https://github.com/sasjs/server/commit/fe3e5088f8dfff50042ec8e8aac9ba5ba1394deb))
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)
### Bug Fixes
* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](https://github.com/sasjs/server/commit/40f95f9072c8685910138d88fd2410f8704fc975))
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)
### Bug Fixes
* made files extensions case insensitive ([2496043](https://github.com/sasjs/server/commit/249604384e42be4c12c88c70a7dff90fc1917a8f))
## [0.21.4](https://github.com/sasjs/server/compare/v0.21.3...v0.21.4) (2022-09-21)
### Bug Fixes
* removing single quotes from _program value ([a0e7875](https://github.com/sasjs/server/commit/a0e7875ae61cbb6e7d3995d2e36e7300b0daec86))
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)
### Bug Fixes
* return same tokens if not expired ([330c020](https://github.com/sasjs/server/commit/330c020933f1080261b38f07d6b627f6d7c62446))
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)
### Bug Fixes
* default content-type for sas programs should be text/plain ([9977c9d](https://github.com/sasjs/server/commit/9977c9d161947b11d45ab2513f99a5320a3f5a06))
* **studio:** inject program path to code before sending for execution ([edc2e2a](https://github.com/sasjs/server/commit/edc2e2a302ccea4985f3d6b83ef8c23620ab82b6))
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)
### Bug Fixes
* SASJS_WEBOUT_HEADERS path for windows ([0749d65](https://github.com/sasjs/server/commit/0749d65173e8cfe9a93464711b7be1e123c289ff))
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)
### Features
* sas9 mocker improved - public access denied scenario ([06d3b17](https://github.com/sasjs/server/commit/06d3b1715432ea245ee755ae1dfd0579d3eb30e9))
# [0.20.0](https://github.com/sasjs/server/compare/v0.19.0...v0.20.0) (2022-09-16)
### Features
* add support for R stored programs ([d6651bb](https://github.com/sasjs/server/commit/d6651bbdbeee5067f53c36e69a0eefa973c523b6))
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)
### Features
* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)
### Features
* add option for program launch in context menu ([ee2db27](https://github.com/sasjs/server/commit/ee2db276bb0bbd522f758e0b66f7e7b2f4afd9d5))
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)
### Bug Fixes
* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](https://github.com/sasjs/server/commit/393b5eaf990049c39eecf2b9e8dd21a001b6e298))
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)
### Bug Fixes
* invalid JS logic ([9f06080](https://github.com/sasjs/server/commit/9f06080348aed076f8188a26fb4890d38a5a3510))
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)
### Bug Fixes
* making SASINITIALFOLDER option windows only. Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](https://github.com/sasjs/server/commit/e63271a67a0deb3059a5f2bec1854efee5a6e5a5))
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)
### Bug Fixes
* addition of SASINITIALFOLDER startup option. Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](https://github.com/sasjs/server/commit/a5ee2f292384f90e9d95d003d652311c0d91a7a7))
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)
### Bug Fixes
* typo mistake ([ee17d37](https://github.com/sasjs/server/commit/ee17d37aa188b0ca43cea0e89d6cd1a566b765cb))
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)
### Bug Fixes
* allow underscores in file name ([bce83cb](https://github.com/sasjs/server/commit/bce83cb6fbc98f8198564c9399821f5829acc767))
### Features
* add the functionality of saving file by ctrl + s in editor ([3a3c90d](https://github.com/sasjs/server/commit/3a3c90d9e690ac5267bf1acc834b5b5c5b4dadb6))
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)
### Bug Fixes
* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](https://github.com/sasjs/server/commit/98ea2ac9b98631605e39e5900e533727ea0e3d85))
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)
### Bug Fixes
* add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py ([882bedd](https://github.com/sasjs/server/commit/882bedd5d5da22de6ed45c03d0a261aadfb3a33c))
* update content for code.sas file ([02e88ae](https://github.com/sasjs/server/commit/02e88ae7280d020a753bc2c095a931c79ac392d1))
* update default content type for python and js runtimes ([8780b80](https://github.com/sasjs/server/commit/8780b800a34aa618631821e5d97e26e8b0f15806))
### Features
* implement the logic for running python stored programs ([b06993a](https://github.com/sasjs/server/commit/b06993ab9ea24b28d9e553763187387685aaa666))
## [0.15.3](https://github.com/sasjs/server/compare/v0.15.2...v0.15.3) (2022-08-11)
### Bug Fixes
* adding proc printto in precode to enable print output in log. Closes [#253](https://github.com/sasjs/server/issues/253) ([f8bb732](https://github.com/sasjs/server/commit/f8bb7327a8a4649ac77bb6237e31cea075d46bb9))
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)
### Bug Fixes
* remove vulnerabitities ([f27ac51](https://github.com/sasjs/server/commit/f27ac51fc4beb21070d0ab551cfdaec1f6ba39e0))
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)
### Bug Fixes
* **web:** fix UI responsiveness ([d99fdd1](https://github.com/sasjs/server/commit/d99fdd1ec7991b94a0d98338d7a7a6216f46ce45))
# [0.15.0](https://github.com/sasjs/server/compare/v0.14.1...v0.15.0) (2022-08-05)
### Bug Fixes
* after selecting file in sidebar collapse sidebar in mobile view ([e215958](https://github.com/sasjs/server/commit/e215958b8b05d7a8ce9d82395e0640b5b37fb40d))
* improve mobile view for studio page ([c67d3ee](https://github.com/sasjs/server/commit/c67d3ee2f102155e2e9781e13d5d33c1ab227cb4))
* improve responsiveness for mobile view ([6ef40b9](https://github.com/sasjs/server/commit/6ef40b954a87ebb0a2621119064f38d58ea85148))
* improve user experience for adding permissions ([7a162ed](https://github.com/sasjs/server/commit/7a162eda8fc60383ff647d93e6611799e2e6af7a))
* show logout button only when user is logged in ([9227cd4](https://github.com/sasjs/server/commit/9227cd449dc46fd960a488eb281804a9b9ffc284))
### Features
* add multiple permission for same combination of type and principal at once ([754704b](https://github.com/sasjs/server/commit/754704bca89ecbdbcc3bd4ef04b94124c4f24167))
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)
### Bug Fixes
* **apps:** App Stream logo fix ([87c03c5](https://github.com/sasjs/server/commit/87c03c5f8dbdfc151d4ff3722ecbcd3f7e409aea))
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](https://github.com/sasjs/server/commit/77f8d30baf9b1077279c29f1c3e5ca02a5436bc0))
* **env:** check added for not providing WHITELIST ([5966016](https://github.com/sasjs/server/commit/5966016853369146b27ac5781808cb51d65c887f))
* **web:** show login on logged-out state ([f7fcc77](https://github.com/sasjs/server/commit/f7fcc7741aa2af93a4a2b1e651003704c9bbff0c))
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)
### Bug Fixes
* add restriction on add/remove user to public group ([d3a516c](https://github.com/sasjs/server/commit/d3a516c36e45aa1cc76c30c744e6a0e5bd553165))
* call jwt.verify in synchronous way ([254bc07](https://github.com/sasjs/server/commit/254bc07da744a9708109bfb792be70aa3f6284f4))
### Features
* add public group to DB on seed ([c3e3bef](https://github.com/sasjs/server/commit/c3e3befc17102ee1754e1403193040b4f79fb2a7))
* bypass authentication when route is enabled for public group ([68515f9](https://github.com/sasjs/server/commit/68515f95a65d422e29c0ed6028f3ea0ae8d9b1bf))
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)
### Bug Fixes
* show non-admin user his own permissions only ([8a3054e](https://github.com/sasjs/server/commit/8a3054e19ade82e2792cfb0f2a8af9e502c5eb52))
* update schema of Permission ([5d5a9d3](https://github.com/sasjs/server/commit/5d5a9d3788281d75c56f68f0dff231abc9c9c275))
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)
### Bug Fixes
* adding ls=max to reduce log size and improve readability ([916947d](https://github.com/sasjs/server/commit/916947dffacd902ff23ac3e899d1bf5ab6238b75))
## [0.13.1](https://github.com/sasjs/server/compare/v0.13.0...v0.13.1) (2022-07-31)
### Bug Fixes
* adding options to prevent unwanted windows on windows. Closes [#244](https://github.com/sasjs/server/issues/244) ([77db14c](https://github.com/sasjs/server/commit/77db14c690e18145d733ac2b0d646ab0dbe4d521))
# [0.13.0](https://github.com/sasjs/server/compare/v0.12.1...v0.13.0) (2022-07-28)
### Bug Fixes
* autofocus input field and submit on enter ([7681722](https://github.com/sasjs/server/commit/7681722e5afdc2df0c9eed201b05add3beda92a7))
* move api button to user menu ([8de032b](https://github.com/sasjs/server/commit/8de032b5431b47daabcf783c47ff078bf817247d))
### Features
* add action and command to editor ([706e228](https://github.com/sasjs/server/commit/706e228a8e1924786fd9dc97de387974eda504b1))
## [0.12.1](https://github.com/sasjs/server/compare/v0.12.0...v0.12.1) (2022-07-26)
### Bug Fixes
* **web:** disable launch icon button when file content is not saved ([c574b42](https://github.com/sasjs/server/commit/c574b4223591c4a6cd3ef5e146ce99cd8f7c9190))
* **web:** saveAs functionality fixed in studio page ([3c987c6](https://github.com/sasjs/server/commit/3c987c61ddc258f991e2bf38c1f16a0c4248d6ae))
* **web:** show original name as default name in rename file/folder modal ([9640f65](https://github.com/sasjs/server/commit/9640f6526496f3564664ccb1f834d0f659dcad4e))
* **web:** webout tab item fixed in studio page ([7cdffe3](https://github.com/sasjs/server/commit/7cdffe30e36e5cad0284f48ea97925958e12704c))
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](https://github.com/sasjs/server/commit/3b1fcb937d06d02ab99c9e8dbe307012d48a7a3a))
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)
### Bug Fixes
* fileTree api response to include an additional attribute isFolder ([0f19384](https://github.com/sasjs/server/commit/0f193849994f1ac8a071afa8f10af5b46f86663d))
* remove drive component ([06d7c91](https://github.com/sasjs/server/commit/06d7c91fc34620a954df1fd1c682eff370f79ca6))
### Features
* add api end point for delete folder ([08e0c61](https://github.com/sasjs/server/commit/08e0c61e0fd7041d6cded6f4d71fbb410e5615ce))
* add sidebar(drive) to left of studio ([6c35412](https://github.com/sasjs/server/commit/6c35412d2f5180d4e49b12e616576d8b8dacb7d8))
* created api endpoint for adding empty folder in drive ([941917e](https://github.com/sasjs/server/commit/941917e508ece5009135f9dddf99775dd4002f78))
* implemented api for renaming file/folder ([fdcaba9](https://github.com/sasjs/server/commit/fdcaba9d56cddea5d56d7de5a172f1bb49be3db5))
* implemented delete file/folder functionality ([177675b](https://github.com/sasjs/server/commit/177675bc897416f7994dd849dc7bb11ba072efe9))
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](https://github.com/sasjs/server/commit/0ce94a553e53bfcdbd6273b26b322095a080a341))
* implemented the functionality for renaming file/folder from context menu ([7010a6a](https://github.com/sasjs/server/commit/7010a6a1201720d0eb4093267a344fb828b90a2f))
* prevent user from leaving studio page when there are unsaved changes ([6c75502](https://github.com/sasjs/server/commit/6c7550286b5f505e9dfe8ca63c62fa1db1b60b2e))
* **web:** add difference view editor in studio ([420a61a](https://github.com/sasjs/server/commit/420a61a5a6b11dcb5eb0a652ea9cecea5c3bee5f))
## [0.11.5](https://github.com/sasjs/server/compare/v0.11.4...v0.11.5) (2022-07-19)
### Bug Fixes
* Revert "fix(security): missing cookie flags are added" ([ce5218a](https://github.com/sasjs/server/commit/ce5218a2278cc750f2b1032024685dc6cd72f796))
## [0.11.4](https://github.com/sasjs/server/compare/v0.11.3...v0.11.4) (2022-07-19)
### Bug Fixes
* **security:** missing cookie flags are added ([526402f](https://github.com/sasjs/server/commit/526402fd73407ee4fa2d31092111a7e6a1741487))
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)
### Bug Fixes
* filePath fix in code.js file for windows ([2995121](https://github.com/sasjs/server/commit/299512135d77c2ac9e34853cf35aee6f2e1d4da4))
## [0.11.2](https://github.com/sasjs/server/compare/v0.11.1...v0.11.2) (2022-07-18)
### Bug Fixes
* apply icon option only for sas.exe ([d2ddd8a](https://github.com/sasjs/server/commit/d2ddd8aacadfdd143026881f2c6ae8c6b277610a))
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)
### Bug Fixes
* bank operator ([aa02741](https://github.com/sasjs/server/commit/aa027414ed3ce51f1014ef36c4191e064b2e963d))
* ensuring nosplash option only applies for sas.exe ([65e6de9](https://github.com/sasjs/server/commit/65e6de966383fe49a919b1f901d77c7f1e402c9b)), closes [#229](https://github.com/sasjs/server/issues/229)
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)
### Bug Fixes
* **logs:** logs location is configurable ([e024a92](https://github.com/sasjs/server/commit/e024a92f165990e08db8aa26ee326dbcb30e2e46))
### Features
* **logs:** logs to file with rotating + code split into files ([92fda18](https://github.com/sasjs/server/commit/92fda183f3f0f3956b7c791669eb8dd52c389d1b))
# [0.10.0](https://github.com/sasjs/server/compare/v0.9.0...v0.10.0) (2022-07-06)
### Bug Fixes
* add authorize middleware for appStreams ([e54a09d](https://github.com/sasjs/server/commit/e54a09db19ec8690e54a40760531a4e06d250974))
* add isAdmin attribute to return response of get session and login requests ([bdf63df](https://github.com/sasjs/server/commit/bdf63df1d915892486005ec904807749786b1c0c))
* add permission authorization middleware to only specific routes ([f3dfc70](https://github.com/sasjs/server/commit/f3dfc7083fbfb4b447521341b1a86730fb90b4c0))
* bumping core and running lint ([a2d1396](https://github.com/sasjs/server/commit/a2d13960578014312d2cb5e03145bfd1829d99ec))
* controller fixed for deleting permission ([b5f595a](https://github.com/sasjs/server/commit/b5f595a25c50550d62482409353c7629c5a5c3e0))
* do not show admin users in add permission modal ([a75edba](https://github.com/sasjs/server/commit/a75edbaa327ec2af49523c13996ac283061da7d8))
* export GroupResponse interface ([38a7db8](https://github.com/sasjs/server/commit/38a7db8514de0acd94d74ba96bc1efb732add30c))
* move permission filter modal to separate file and icons for different actions ([d000f75](https://github.com/sasjs/server/commit/d000f7508f6d7384afffafee4179151fca802ca8))
* principalId type changed to number from any ([4fcc191](https://github.com/sasjs/server/commit/4fcc191ce9edc7e4dcd8821fb8019f4eea5db4ea))
* remove clientId from principal types ([0781ddd](https://github.com/sasjs/server/commit/0781ddd64e3b5e5ca39647bb4e4e1a9332a0f4f8))
* remove duplicates principals from permission filter modal ([5b319f9](https://github.com/sasjs/server/commit/5b319f9ad1f941b306db6b9473a2128b2e42bf76))
* show loading spinner in studio while executing code ([496247d](https://github.com/sasjs/server/commit/496247d0b9975097a008cf4d3a999d77648fd930))
* show permission component only in server mode ([f863b81](https://github.com/sasjs/server/commit/f863b81a7d40a1296a061ec93946f204382af2c3))
* update permission model ([39fc908](https://github.com/sasjs/server/commit/39fc908de1945f2aaea18d14e6bce703f6bf0c06))
* update permission response ([e516b77](https://github.com/sasjs/server/commit/e516b7716da5ff7e23350a5f77cfa073b1171175))
* **web:** only admin should be able to add, update or delete permission ([be8635c](https://github.com/sasjs/server/commit/be8635ccc5eb34c3f0a5951c8a0421292ef69c97))
### Features
* add api endpoint for deleting permission ([0171344](https://github.com/sasjs/server/commit/01713440a4fa661b76368785c0ca731f096ac70a))
* add api endpoint for updating permission setting ([540f54f](https://github.com/sasjs/server/commit/540f54fb77b364822da7889dbe75c02242f48a59))
* add authorize middleware for validating permissions ([7d916ec](https://github.com/sasjs/server/commit/7d916ec3e9ef579dde1b73015715cd01098c2018))
* add basic UI for settings and permissions ([5652325](https://github.com/sasjs/server/commit/56523254525a66e756196e90b39a2b8cdadc1518))
* add documentation link under usename dropdown menu ([eeb63b3](https://github.com/sasjs/server/commit/eeb63b330c292afcdd5c8f006882b224c4235068))
* add permission model ([6bea1f7](https://github.com/sasjs/server/commit/6bea1f76668ddb070ad95b3e02c31238af67c346))
* add UI for updating permission ([e8c21a4](https://github.com/sasjs/server/commit/e8c21a43b215f5fced0463b70747cda1191a4e01))
* add validation for registering permission ([e5200c1](https://github.com/sasjs/server/commit/e5200c1000903185dfad9ee49c99583e473c4388))
* add, remove and update permissions from web component ([97ecfdc](https://github.com/sasjs/server/commit/97ecfdc95563c72dbdecaebcb504e5194250a763))
* added get authorizedRoutes api endpoint ([b10e932](https://github.com/sasjs/server/commit/b10e9326058193dd65a57fab2d2f05b7b06096e7))
* created modal for adding permission ([1413b18](https://github.com/sasjs/server/commit/1413b1850838ecc988ab289da4541bde36a9a346))
* defined register permission and get all permissions api endpoints ([1103ffe](https://github.com/sasjs/server/commit/1103ffe07b88496967cb03683b08f058ca3bbb9f))
* update swagger docs ([797c2bc](https://github.com/sasjs/server/commit/797c2bcc39005a05a995be15a150d584fecae259))
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)
### Features
* removed secrets from env variables ([9c3da56](https://github.com/sasjs/server/commit/9c3da56901672a818f54267f9defc9f4701ab7fb))
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
### Bug Fixes
* **deploy:** extract first json from zip file ([e290751](https://github.com/sasjs/server/commit/e290751c872d24009482871a8c398e834357dcde))
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)
### Bug Fixes
* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](https://github.com/sasjs/server/commit/5cc85b57f80b13296156811fe966d7b37d45f213))
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)
### Bug Fixes
* make CA_ROOT optional in getCertificates method ([1b5859e](https://github.com/sasjs/server/commit/1b5859ee37ae73c419115b9debfd5141a79733de))
* update /logout route to /SASLogon/logout ([65380be](https://github.com/sasjs/server/commit/65380be2f3945bae559f1749064845b514447a53))
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)
### Features
* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](https://github.com/sasjs/server/commit/2119e9de9ab1e5ce1222658f554ac74f4f35cf4d))
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)
### Bug Fixes
* path descriptions and defaults ([5d5d6ce](https://github.com/sasjs/server/commit/5d5d6ce3265a43af2e22bcd38cda54fafaf7b3ef))
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)
### Bug Fixes
* removing UTF-8 options from commandline. There appears to be no reliable way to enforce ([f6dc74f](https://github.com/sasjs/server/commit/f6dc74f16bddafa1de9c83c2f27671a241abdad4))
## [0.7.1](https://github.com/sasjs/server/compare/v0.7.0...v0.7.1) (2022-06-20)

19
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,19 @@
## Issue
Link any related issue(s) in this section.
## Intent
What this PR intends to achieve.
## Implementation
What code changes have been made to achieve the intent.
## Checks
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] Any new functionality has been unit tested.
- [ ] All unit tests are passing (`npm test`).
- [ ] All CI checks are green.
- [ ] Reviewer is assigned.

View File

@@ -64,23 +64,53 @@ Example contents of a `.env` file:
# Server mode is multi-user and suitable for intranet / internet use
MODE=
# A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas, js, py and r
# This string sets the priority of the available analytic runtimes
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
# For each option provided, there should be a corresponding path,
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
# Priority is given to runtimes earlier in the string
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
RUN_TIMES=
# Path to SAS executable (sas.exe / sas.sh)
SAS_PATH=/path/to/sas/executable.exe
# Path to Node.js executable
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
# Path to Python executable
PYTHON_PATH=/usr/bin/python
# Path to R executable
R_PATH=/usr/bin/Rscript
# Path to working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root
# This location is for files, sasjs packages and appStreamConfig.json
DRIVE_LOCATION=./sasjs_root/drive
# options: [http|https] default: http
PROTOCOL=
# default: 5000
PORT=
# options: [sas9|sasviya]
# If not present, mocking function is disabled
MOCK_SERVERTYPE=
# default: /api/mocks
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
# Server will automatically use subdirectory accordingly
STATIC_MOCK_LOCATION=
#
## Additional SAS Options
@@ -99,17 +129,27 @@ SASV9_OPTIONS= -NOXCMD
## Additional Web Server Options
#
# ENV variables required for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem
# ENV variables for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem (required)
CERT_CHAIN=certificate.pem (required)
CA_ROOT=fullchain.pem (optional)
# ENV variables required for MODE: `server`
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret>
## ENV variables required for MODE: `server`
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
# options: [mongodb|cosmos_mongodb] default: mongodb
DB_TYPE=
# AUTH_PROVIDERS options: [ldap] default: ``
AUTH_PROVIDERS=
## ENV variables required for AUTH_MECHANISM: `ldap`
LDAP_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
LDAP_BIND_PASSWORD = <password>
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
@@ -118,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=
@@ -135,16 +175,38 @@ HELMET_COEP=
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
# To prevent brute force attack on login route we have implemented rate limiter
# Only valid for MODE: server
# Following are configurable env variable rate limiter
# After this, access is blocked for 1 day
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
# After this, access is blocked for an hour
# 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;
# Name of the admin user that will be created on startup if not exists already
# Default is `secretuser`
ADMIN_USERNAME=secretuser
# Temporary password for the ADMIN_USERNAME, which is in place until the first login
# Default is `secretpassword`
ADMIN_PASSWORD_INITIAL=secretpassword
# Specify whether app has to reset the ADMIN_USERNAME's password or not
# Default is NO. Possible options are YES and NO
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
ADMIN_PASSWORD_RESET=NO
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=
# A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas and js
# options: [sas,js|js,sas|sas|js] default:sas
RUN_TIMES=
# This location is for server logs with classical UNIX logrotate behavior
LOG_LOCATION=./sasjs_root/logs
```

View File

@@ -1,26 +1,47 @@
MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
ALLOWED_DOMAIN=<just domain e.g. example.com >
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem
CERT_CHAIN=certificate.pem
CA_ROOT=fullchain.pem
PORT=[5000] default value is 5000
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
HELMET_COEP=[true|false] if omitted HELMET default will be used
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
DB_TYPE=[mongodb|cosmos_mongodb] default considered as mongodb
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
AUTH_PROVIDERS=[ldap]
LDAP_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
LDAP_BIND_PASSWORD = <password>
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
#default value is 10
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
ADMIN_USERNAME=secretuser
ADMIN_PASSWORD_INITIAL=secretpassword
ADMIN_PASSWORD_RESET=NO
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
PYTHON_PATH=/usr/bin/python
R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root
DRIVE_LOCATION=./sasjs_root/drive
LOG_FORMAT_MORGAN=common
LOG_FORMAT_MORGAN=common
LOG_LOCATION=./sasjs_root/logs

View File

@@ -0,0 +1 @@
You have signed in.

View File

@@ -0,0 +1 @@
You have signed out.

View File

@@ -0,0 +1,30 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1" />
</head>
<div class="content">
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
<!--form container-->
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
<span class="userid">
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
</span>
<span class="password">
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
</span>
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
</form>
</div>
</html>

View File

@@ -0,0 +1 @@
Public access has been denied.

View File

@@ -0,0 +1 @@
"title": "Log Off SAS Demo User"

20290
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,10 @@
"description": "Api of SASjs server",
"main": "./src/server.ts",
"scripts": {
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore && npm run downloadMacros",
"prestart": "npm run initial",
"prebuild": "npm run initial",
"start": "nodemon ./src/server.ts",
"start": "NODE_ENV=development nodemon ./src/server.ts",
"start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc",
"postbuild": "npm run copy:files",
@@ -17,20 +17,21 @@
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"exe": "npm run build && pkg .",
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sas:copy && npm run web:copy",
"public:copy": "cp -r ./public/ ./build/public/",
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
"sas:copy": "cp -r ./sas/ ./build/sas/",
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts",
"downloadMacros": "ts-node ./scripts/downloadMacros.ts"
},
"bin": "./build/src/server.js",
"pkg": {
"assets": [
"./build/public/**/*",
"./build/sasjsbuild/**/*",
"./build/sasjscore/**/*",
"./build/sas/**/*",
"./web/build/**/*"
],
"targets": [
@@ -47,24 +48,25 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1",
"@sasjs/core": "^4.59.7",
"@sasjs/utils": "^3.5.2",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"express": "^4.21.2",
"express-session": "^1.18.2",
"helmet": "^5.0.2",
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"jsonwebtoken": "^9.0.2",
"ldapjs": "2.3.3",
"mongoose": "^6.13.8",
"morgan": "^1.10.1",
"multer": "^1.4.5-lts.1",
"rate-limiter-flexible": "2.4.1",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"unzipper": "^0.12.3",
"url": "^0.10.3"
},
"devDependencies": {
@@ -72,32 +74,34 @@
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.5",
"@types/mongoose-sequence": "^3.0.6",
"@types/ldapjs": "^2.2.4",
"@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
"@types/node": "^20.0.0",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"dotenv": "^10.0.0",
"axios": "^1.12.2",
"csrf": "^3.1.0",
"dotenv": "^16.0.1",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7",
"jest": "^29.7.0",
"mongodb-memory-server": "8.11.4",
"nodejs-file-downloader": "4.10.2",
"nodemon": "^3.0.0",
"pkg": "5.6.0",
"prettier": "^2.3.1",
"prettier": "^3.0.0",
"rimraf": "^3.0.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-jest": "^29.1.0",
"ts-node": "^10.0.0",
"tsoa": "3.14.1",
"typescript": "^4.3.2"
"typescript": "^5.0.0"
},
"nodemonConfig": {
"ignore": [

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,12 @@ import {
readFile,
SASJsFileType
} from '@sasjs/utils'
import { apiRoot, sysInitCompiledPath } from '../src/utils'
import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
const compiledSystemInit = async (systemInit: string) =>
'options ps=max;\n' +
'options ls=max ps=max;\n' +
(await loadDependenciesFile({
fileContent: systemInit,
type: SASJsFileType.job,

View File

@@ -8,7 +8,11 @@ import {
listFilesInFolder
} from '@sasjs/utils'
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
import {
apiRoot,
sasJSCoreMacros,
sasJSCoreMacrosInfo
} from '../src/utils/file'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')

View File

@@ -0,0 +1,39 @@
import axios from 'axios'
import Downloader from 'nodejs-file-downloader'
import { createFile, listFilesInFolder } from '@sasjs/utils'
import { sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils/file'
export const downloadMacros = async () => {
const url =
'https://api.github.com/repos/yabwon/SAS_PACKAGES/contents/SPF/Macros'
console.info(`Downloading macros from ${url}`)
await axios
.get(url)
.then(async (res) => {
await downloadFiles(res.data)
})
.catch((err) => {
throw new Error(err)
})
}
const downloadFiles = async function (fileList: any) {
for (const file of fileList) {
const downloader = new Downloader({
url: file.download_url,
directory: sasJSCoreMacros,
fileName: file.path.replace(/^SPF\/Macros/, ''),
cloneFiles: false
})
await downloader.download()
}
const fileNames = await listFilesInFolder(sasJSCoreMacros)
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
}
downloadMacros()

View File

@@ -0,0 +1,21 @@
import { Express } from 'express'
import cors from 'cors'
import { CorsType } from '../utils'
export const configureCors = (app: Express) => {
const { CORS, WHITELIST } = process.env
if (CORS === CorsType.ENABLED) {
const whiteList: string[] = []
WHITELIST?.split(' ')
?.filter((url) => !!url)
.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
process.logger.info('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
}

View File

@@ -0,0 +1,48 @@
import { Express, CookieOptions } from 'express'
import mongoose from 'mongoose'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import { DatabaseType, ModeType, ProtocolType } from '../utils'
export const configureExpressSession = (app: Express) => {
const { MODE, DB_TYPE } = process.env
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
if (process.env.NODE_ENV !== 'test') {
if (DB_TYPE === DatabaseType.COSMOS_MONGODB) {
// COSMOS DB requires specific connection options (compatibility mode)
// See: https://www.npmjs.com/package/connect-mongo#set-the-compatibility-mode
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
autoRemove: 'interval'
})
} else {
store = MongoStore.create({
client: mongoose.connection!.getClient() as any
})
}
}
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: ALLOWED_DOMAIN?.trim() || undefined
}
app.use(
session({
secret: process.secrets.SESSION_SECRET,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store,
cookie: cookieOptions
})
)
}
}

View File

@@ -0,0 +1,33 @@
import path from 'path'
import { Express } from 'express'
import morgan from 'morgan'
import { createStream } from 'rotating-file-stream'
import { generateTimestamp } from '@sasjs/utils'
import { getLogFolder } from '../utils'
export const configureLogger = (app: Express) => {
const { LOG_FORMAT_MORGAN } = process.env
let options
if (
process.env.NODE_ENV !== 'development' &&
process.env.NODE_ENV !== 'test'
) {
const timestamp = generateTimestamp()
const filename = `${timestamp}.log`
const logsFolder = getLogFolder()
// create a rotating write stream
var accessLogStream = createStream(filename, {
interval: '1d', // rotate daily
path: logsFolder
})
process.logger.info('Writing Logs to :', path.join(logsFolder, filename))
options = { stream: accessLogStream }
}
// setup the logger
app.use(morgan(LOG_FORMAT_MORGAN as string, options))
}

View File

@@ -0,0 +1,26 @@
import { Express } from 'express'
import { getEnvCSPDirectives } from '../utils/parseHelmetConfig'
import { HelmetCoepType, ProtocolType } from '../utils'
import helmet from 'helmet'
export const configureSecurity = (app: Express) => {
const { PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } = process.env
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
HELMET_CSP_CONFIG_PATH
)
if (PROTOCOL === ProtocolType.HTTP)
cspConfigJson['upgrade-insecure-requests'] = null
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
...cspConfigJson
}
},
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
})
)
}

View File

@@ -0,0 +1,4 @@
export * from './configureCors'
export * from './configureExpressSession'
export * from './configureLogger'
export * from './configureSecurity'

View File

@@ -1,30 +1,30 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import csrf from 'csurf'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import morgan from 'morgan'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import cors from 'cors'
import helmet from 'helmet'
import {
connectDB,
copySASjsCore,
CorsType,
createWeboutSasFile,
getFilesFolder,
getPackagesFolder,
getWebBuildFolder,
HelmetCoepType,
instantiateLogger,
loadAppStreamConfig,
ModeType,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders,
setupFilesFolder,
setupPackagesFolder,
setupUserAutoExec,
verifyEnvVariables
} from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
import {
configureCors,
configureExpressSession,
configureLogger,
configureSecurity
} from './app-modules'
import { folderExists } from '@sasjs/utils'
dotenv.config()
@@ -34,110 +34,55 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
app.use(cookieParser())
const {
MODE,
CORS,
WHITELIST,
PROTOCOL,
HELMET_CSP_CONFIG_PATH,
HELMET_COEP,
LOG_FORMAT_MORGAN
} = process.env
app.use(morgan(LOG_FORMAT_MORGAN as string))
export const cookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
HELMET_CSP_CONFIG_PATH
)
if (PROTOCOL === ProtocolType.HTTP)
cspConfigJson['upgrade-insecure-requests'] = null
/***********************************
* CSRF Protection *
***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions })
/***********************************
* Handle security and origin *
***********************************/
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
...cspConfigJson
}
},
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
})
)
/***********************************
* Enabling CORS *
***********************************/
if (CORS === CorsType.ENABLED) {
const whiteList: string[] = []
WHITELIST?.split(' ')
?.filter((url) => !!url)
.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
/***********************************
* DB Connection & *
* Express Sessions *
* With Mongo Store *
***********************************/
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
// NOTE: when exporting app.js as agent for supertest
// we should exclude connecting to the real database
if (process.env.NODE_ENV !== 'test') {
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
}
app.use(
session({
secret: process.env.SESSION_SECRET as string,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store,
cookie: cookieOptions
})
)
}
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack)
process.logger.error(err.stack)
res.status(500).send('Something broke!')
}
export default setProcessVariables().then(async () => {
await setupFolders()
await copySASjsCore()
app.use(cookieParser())
configureLogger(app)
/***********************************
* Handle security and origin *
***********************************/
configureSecurity(app)
/***********************************
* Enabling CORS *
***********************************/
configureCors(app)
/***********************************
* DB Connection & *
* Express Sessions *
* With Mongo Store *
***********************************/
configureExpressSession(app)
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
// Body parser is used for decoding the formdata on POST request.
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
app.use(express.urlencoded({ extended: true }))
await setupUserAutoExec()
if (!(await folderExists(getFilesFolder()))) await setupFilesFolder()
if (!(await folderExists(getPackagesFolder()))) await setupPackagesFolder()
const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos')
if (await folderExists(sasautosPath)) {
process.logger.warn(
`SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder`
)
} else {
await copySASjsCore()
await createWeboutSasFile()
}
// loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc

View File

@@ -1,12 +1,27 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import express from 'express'
import {
Security,
Route,
Tags,
Example,
Post,
Patch,
Request,
Body,
Query,
Hidden
} from 'tsoa'
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import {
generateAccessToken,
generateRefreshToken,
getTokensFromDB,
removeTokensInDB,
saveTokensInDB
} from '../utils'
import Client from '../model/Client'
import User from '../model/User'
@Route('SASjsApi/auth')
@Tags('Auth')
@@ -60,6 +75,18 @@ export class AuthController {
public async logout(@Query() @Hidden() data?: InfoJWT) {
return logout(data!)
}
/**
* @summary Update user's password.
*/
@Security('bearerAuth')
@Patch('updatePassword')
public async updatePassword(
@Request() req: express.Request,
@Body() body: UpdatePasswordPayload
) {
return updatePassword(req, body)
}
}
const token = async (data: any): Promise<TokenResponse> => {
@@ -73,8 +100,26 @@ const token = async (data: any): Promise<TokenResponse> => {
AuthController.deleteCode(userInfo.userId, clientId)
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
// get tokens from DB
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
if (existingTokens) {
return {
accessToken: existingTokens.accessToken,
refreshToken: existingTokens.refreshToken
}
}
const client = await Client.findOne({ clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
@@ -82,8 +127,17 @@ const token = async (data: any): Promise<TokenResponse> => {
}
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
const client = await Client.findOne({ clientId: userInfo.clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
await saveTokensInDB(
userInfo.userId,
@@ -99,6 +153,40 @@ const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
const updatePassword = async (
req: express.Request,
data: UpdatePasswordPayload
) => {
const { currentPassword, newPassword } = data
const userId = req.user?.userId
const dbUser = await User.findOne({ id: userId })
if (!dbUser)
throw {
code: 404,
message: `User not found!`
}
if (dbUser?.authProvider) {
throw {
code: 405,
message:
'Can not update password of user that is created by an external auth provider.'
}
}
const validPass = dbUser.comparePassword(currentPassword)
if (!validPass)
throw {
code: 403,
message: `Invalid current password!`
}
dbUser.password = User.hashPassword(newPassword)
dbUser.needsToUpdatePassword = false
await dbUser.save()
}
interface TokenPayload {
/**
* Client ID
@@ -125,17 +213,31 @@ interface TokenResponse {
refreshToken: string
}
interface UpdatePasswordPayload {
/**
* Current Password
* @example "currentPasswordString"
*/
currentPassword: string
/**
* New Password
* @example "newPassword"
*/
newPassword: string
}
const verifyAuthCode = async (
clientId: string,
code: string
): Promise<InfoJWT | undefined> => {
return new Promise((resolve, reject) => {
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
return new Promise((resolve) => {
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
if (err) return resolve(undefined)
const payload = data as InfoJWT
const clientInfo: InfoJWT = {
clientId: data?.clientId,
userId: data?.userId
clientId: payload?.clientId,
userId: payload?.userId
}
if (clientInfo.clientId === clientId) {
return resolve(clientInfo)

View File

@@ -0,0 +1,186 @@
import express from 'express'
import { Security, Route, Tags, Get, Post, Example } from 'tsoa'
import { LDAPClient, LDAPUser, LDAPGroup, AuthProviderType } from '../utils'
import { randomBytes } from 'crypto'
import User from '../model/User'
import Group from '../model/Group'
import Permission from '../model/Permission'
@Security('bearerAuth')
@Route('SASjsApi/authConfig')
@Tags('Auth_Config')
export class AuthConfigController {
/**
* @summary Gives the detail of Auth Mechanism.
*
*/
@Example({
ldap: {
LDAP_URL: 'ldaps://my.ldap.server:636',
LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron',
LDAP_BIND_PASSWORD: 'secret',
LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron',
LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'
}
})
@Get('/')
public getDetail() {
return getAuthConfigDetail()
}
/**
* @summary Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.
*
*/
@Example({
users: 5,
groups: 3
})
@Post('/synchroniseWithLDAP')
public async synchroniseWithLDAP() {
return synchroniseWithLDAP()
}
}
const synchroniseWithLDAP = async () => {
process.logger.info('Syncing LDAP with internal DB')
const permissions = await Permission.get({})
await Permission.deleteMany()
await User.deleteMany({ authProvider: AuthProviderType.LDAP })
await Group.deleteMany({ authProvider: AuthProviderType.LDAP })
const ldapClient = await LDAPClient.init()
process.logger.info('fetching LDAP users')
const users = await ldapClient.getAllLDAPUsers()
process.logger.info('inserting LDAP users to DB')
const existingUsers: string[] = []
const importedUsers: LDAPUser[] = []
for (const user of users) {
const usernameExists = await User.findOne({ username: user.username })
if (usernameExists) {
existingUsers.push(user.username)
continue
}
const hashPassword = User.hashPassword(randomBytes(64).toString('hex'))
await User.create({
displayName: user.displayName,
username: user.username,
password: hashPassword,
authProvider: AuthProviderType.LDAP,
needsToUpdatePassword: false
})
importedUsers.push(user)
}
if (existingUsers.length > 0) {
process.logger.info(
'Failed to insert following users as they already exist in DB:'
)
existingUsers.forEach((user) => process.logger.log(`* ${user}`))
}
process.logger.info('fetching LDAP groups')
const groups = await ldapClient.getAllLDAPGroups()
process.logger.info('inserting LDAP groups to DB')
const existingGroups: string[] = []
const importedGroups: LDAPGroup[] = []
for (const group of groups) {
const groupExists = await Group.findOne({ name: group.name })
if (groupExists) {
existingGroups.push(group.name)
continue
}
await Group.create({
name: group.name,
authProvider: AuthProviderType.LDAP
})
importedGroups.push(group)
}
if (existingGroups.length > 0) {
process.logger.info(
'Failed to insert following groups as they already exist in DB:'
)
existingGroups.forEach((group) => process.logger.log(`* ${group}`))
}
process.logger.info('associating users and groups')
for (const group of importedGroups) {
const dbGroup = await Group.findOne({ name: group.name })
if (dbGroup) {
for (const member of group.members) {
const user = importedUsers.find((user) => user.uid === member)
if (user) {
const dbUser = await User.findOne({ username: user.username })
if (dbUser) await dbGroup.addUser(dbUser)
}
}
}
}
process.logger.info('setting permissions')
for (const permission of permissions) {
const newPermission = new Permission({
path: permission.path,
type: permission.type,
setting: permission.setting
})
if (permission.user) {
const dbUser = await User.findOne({ username: permission.user.username })
if (dbUser) newPermission.user = dbUser._id
} else if (permission.group) {
const dbGroup = await Group.findOne({ name: permission.group.name })
if (dbGroup) newPermission.group = dbGroup._id
}
await newPermission.save()
}
process.logger.info('LDAP synchronization completed!')
return {
userCount: importedUsers.length,
groupCount: importedGroups.length
}
}
const getAuthConfigDetail = () => {
const { AUTH_PROVIDERS } = process.env
const returnObj: any = {}
if (AUTH_PROVIDERS === AuthProviderType.LDAP) {
const {
LDAP_URL,
LDAP_BIND_DN,
LDAP_BIND_PASSWORD,
LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN
} = process.env
returnObj.ldap = {
LDAP_URL: LDAP_URL ?? '',
LDAP_BIND_DN: LDAP_BIND_DN ?? '',
LDAP_BIND_PASSWORD: LDAP_BIND_PASSWORD ?? '',
LDAP_USERS_BASE_DN: LDAP_USERS_BASE_DN ?? '',
LDAP_GROUPS_BASE_DN: LDAP_GROUPS_BASE_DN ?? ''
}
}
return returnObj
}

View File

@@ -1,18 +1,27 @@
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
import { Security, Route, Tags, Example, Post, Body, Get } from 'tsoa'
import Client, { ClientPayload } from '../model/Client'
import Client, {
ClientPayload,
NUMBER_OF_SECONDS_IN_A_DAY
} from '../model/Client'
@Security('bearerAuth')
@Route('SASjsApi/client')
@Tags('Client')
export class ClientController {
/**
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
* @summary Admin only task. Create client with the following attributes:
* ClientId,
* ClientSecret,
* accessTokenExpiration (optional),
* refreshTokenExpiration (optional)
*
*/
@Example<ClientPayload>({
clientId: 'someFormattedClientID1234',
clientSecret: 'someRandomCryptoString'
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
})
@Post('/')
public async createClient(
@@ -20,10 +29,37 @@ export class ClientController {
): Promise<ClientPayload> {
return createClient(body)
}
/**
* @summary Admin only task. Returns the list of all the clients
*/
@Example<ClientPayload[]>([
{
clientId: 'someClientID1234',
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'someOtherClientID',
clientSecret: 'someOtherRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
])
@Get('/')
public async getAllClients(): Promise<ClientPayload[]> {
return getAllClients()
}
}
const createClient = async (data: any): Promise<ClientPayload> => {
const { clientId, clientSecret } = data
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
const {
clientId,
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
} = data
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId })
@@ -32,13 +68,27 @@ const createClient = async (data: any): Promise<ClientPayload> => {
// Create a new client
const client = new Client({
clientId,
clientSecret
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
})
const savedClient = await client.save()
return {
clientId: savedClient.clientId,
clientSecret: savedClient.clientSecret
clientSecret: savedClient.clientSecret,
accessTokenExpiration: savedClient.accessTokenExpiration,
refreshTokenExpiration: savedClient.refreshTokenExpiration
}
}
const getAllClients = async (): Promise<ClientPayload[]> => {
return Client.find({}).select({
_id: 0,
clientId: 1,
clientSecret: 1,
accessTokenExpiration: 1,
refreshTokenExpiration: 1
})
}

View File

@@ -1,43 +1,91 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.'
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 {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store code outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
@Tags('Code')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
* Execute Code on the Specified Runtime
* @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(
@Request() request: express.Request,
@Body() body: ExecuteCodePayload
): Promise<ExecuteReturnJsonResponse> {
): 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 (
@@ -51,22 +99,62 @@ const executeCode = async (
: await getUserAutoExec()
try {
const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
returnJson: true,
runTime: runTime
})) as ExecuteReturnJson
const { result } = await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true
})
return {
status: 'success',
_webout: webout as string,
log: parseLogToArray(log),
httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<TriggerCodeResponse> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
try {
// call executeProgram method of ExecutionController without awaiting
new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true,
session // session is provided
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,

View File

@@ -22,6 +22,7 @@ import {
moveFile,
createFolder,
deleteFile as deleteFileOnSystem,
deleteFolder as deleteFolderOnSystem,
folderExists,
listFilesInFolder,
listSubFoldersInFolder,
@@ -58,11 +59,32 @@ interface GetFileTreeResponse {
tree: TreeNode
}
interface UpdateFileResponse {
interface FileFolderResponse {
status: string
message?: string
}
interface AddFolderPayload {
/**
* Location of folder
* @example "/Public/someFolder"
*/
folderPath: string
}
interface RenamePayload {
/**
* Old path of file/folder
* @example "/Public/someFolder"
*/
oldPath: string
/**
* New path of file/folder
* @example "/Public/newFolder"
*/
newPath: string
}
const fileTreeExample = getTreeExample()
const successDeployResponse: DeployResponse = {
@@ -143,7 +165,7 @@ export class DriveController {
/**
*
* @summary Delete file from SASjs Drive
* @query _filePath Location of SAS program
* @query _filePath Location of file
* @example _filePath "/Public/somefolder/some.file"
*/
@Delete('/file')
@@ -151,20 +173,31 @@ export class DriveController {
return deleteFile(_filePath)
}
/**
*
* @summary Delete folder from SASjs Drive
* @query _folderPath Location of folder
* @example _folderPath "/Public/somefolder/"
*/
@Delete('/folder')
public async deleteFolder(@Query() _folderPath: string) {
return deleteFolder(_folderPath)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Create a file in SASjs Drive
* @param _filePath Location of SAS program
* @param _filePath Location of file
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
@Example<FileFolderResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(403, 'File already exists', {
@Response<FileFolderResponse>(403, 'File already exists', {
status: 'failure',
message: 'File request failed.'
})
@@ -173,10 +206,28 @@ export class DriveController {
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
): Promise<FileFolderResponse> {
return saveFile((_filePath ?? filePath)!, file)
}
/**
* @summary Create an empty folder in SASjs Drive
*
*/
@Example<FileFolderResponse>({
status: 'success'
})
@Response<FileFolderResponse>(409, 'Folder already exists', {
status: 'failure',
message: 'Add folder request failed.'
})
@Post('/folder')
public async addFolder(
@Body() body: AddFolderPayload
): Promise<FileFolderResponse> {
return addFolder(body.folderPath)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
@@ -187,10 +238,10 @@ export class DriveController {
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
@Example<FileFolderResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
@Response<FileFolderResponse>(403, `File doesn't exist`, {
status: 'failure',
message: 'File request failed.'
})
@@ -199,10 +250,28 @@ export class DriveController {
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
): Promise<FileFolderResponse> {
return updateFile((_filePath ?? filePath)!, file)
}
/**
* @summary Renames a file/folder in SASjs Drive
*
*/
@Example<FileFolderResponse>({
status: 'success'
})
@Response<FileFolderResponse>(409, 'Folder already exists', {
status: 'failure',
message: 'rename request failed.'
})
@Post('/rename')
public async rename(
@Body() body: RenamePayload
): Promise<FileFolderResponse> {
return rename(body.oldPath, body.newPath)
}
/**
* @summary Fetch file tree within SASjs Drive.
*
@@ -249,20 +318,26 @@ const getFile = async (req: express.Request, filePath: string) => {
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot get file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error("File doesn't exist.")
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
const extension = path.extname(filePathFull).toLowerCase()
if (extension === '.sas') {
req.res?.setHeader('Content-type', 'text/plain')
}
req.res?.sendFile(path.resolve(filePathFull))
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
}
const getFolder = async (folderPath?: string) => {
@@ -273,17 +348,26 @@ const getFolder = async (folderPath?: string) => {
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFilesPath)) {
throw new Error('Cannot get folder outside drive.')
}
if (!folderPathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get folder outside drive.`
}
if (!(await folderExists(folderPathFull))) {
throw new Error("Folder doesn't exist.")
}
if (!(await folderExists(folderPathFull)))
throw {
code: 404,
status: 'Not Found',
message: `Folder doesn't exist.`
}
if (!(await isFolder(folderPathFull))) {
throw new Error('Not a Folder.')
}
if (!(await isFolder(folderPathFull)))
throw {
code: 400,
status: 'Bad Request',
message: 'Not a Folder.'
}
const files: string[] = await listFilesInFolder(folderPathFull)
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
@@ -302,19 +386,51 @@ const deleteFile = async (filePath: string) => {
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot delete file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't delete file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.')
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await deleteFileOnSystem(filePathFull)
return { status: 'success' }
}
const deleteFolder = async (folderPath: string) => {
const driveFolderPath = getFilesFolder()
const folderPathFull = path
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFolderPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't delete folder outside drive.`
}
if (!(await folderExists(folderPathFull)))
throw {
code: 404,
status: 'Not Found',
message: `Folder doesn't exist.`
}
await deleteFolderOnSystem(folderPathFull)
return { status: 'success' }
}
const saveFile = async (
filePath: string,
multerFile: Express.Multer.File
@@ -325,13 +441,19 @@ const saveFile = async (
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot put file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't put file outside drive.`
}
if (await fileExists(filePathFull)) {
throw new Error('File already exists.')
}
if (await fileExists(filePathFull))
throw {
code: 409,
status: 'Conflict',
message: 'File already exists.'
}
const folderPath = path.dirname(filePathFull)
await createFolder(folderPath)
@@ -340,6 +462,88 @@ const saveFile = async (
return { status: 'success' }
}
const addFolder = async (folderPath: string): Promise<FileFolderResponse> => {
const drivePath = getFilesFolder()
const folderPathFull = path
.join(drivePath, folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(drivePath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't put folder outside drive.`
}
if (await folderExists(folderPathFull))
throw {
code: 409,
status: 'Conflict',
message: 'Folder already exists.'
}
await createFolder(folderPathFull)
return { status: 'success' }
}
const rename = async (
oldPath: string,
newPath: string
): Promise<FileFolderResponse> => {
const drivePath = getFilesFolder()
const oldPathFull = path
.join(drivePath, oldPath)
.replace(new RegExp('/', 'g'), path.sep)
const newPathFull = path
.join(drivePath, newPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!oldPathFull.includes(drivePath))
throw {
code: 400,
status: 'Bad Request',
message: `Old path can't be outside of drive.`
}
if (!newPathFull.includes(drivePath))
throw {
code: 400,
status: 'Bad Request',
message: `New path can't be outside of drive.`
}
if (await isFolder(oldPathFull)) {
if (await folderExists(newPathFull))
throw {
code: 409,
status: 'Conflict',
message: 'Folder with new name already exists.'
}
else moveFile(oldPathFull, newPathFull)
return { status: 'success' }
} else if (await fileExists(oldPathFull)) {
if (await fileExists(newPathFull))
throw {
code: 409,
status: 'Conflict',
message: 'File with new name already exists.'
}
else moveFile(oldPathFull, newPathFull)
return { status: 'success' }
}
throw {
code: 404,
status: 'Not Found',
message: 'No file/folder found for provided path.'
}
}
const updateFile = async (
filePath: string,
multerFile: Express.Multer.File
@@ -350,13 +554,19 @@ const updateFile = async (
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot modify file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't modify file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error(`File doesn't exist.`)
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await moveFile(multerFile.path, filePathFull)

View File

@@ -10,8 +10,9 @@ import {
Body
} from 'tsoa'
import Group, { GroupPayload } from '../model/Group'
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import { AuthProviderType } from '../utils'
import { UserResponse } from './user'
export interface GroupResponse {
@@ -20,7 +21,7 @@ export interface GroupResponse {
description: string
}
interface GroupDetailsResponse {
export interface GroupDetailsResponse {
groupId: number
name: string
description: string
@@ -147,12 +148,14 @@ export class GroupController {
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return await group.remove()
}
}
@@ -198,7 +201,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
'groupId name description isActive users -_id'
).populate(
'users',
'id username displayName -_id'
'id username displayName isAdmin -_id'
)) as unknown as GroupDetailsResponse
if (!group)
throw {
@@ -241,6 +244,20 @@ const updateUsersListInGroup = async (
message: 'Group not found.'
}
if (group.name === PUBLIC_GROUP_NAME)
throw {
code: 400,
status: 'Bad Request',
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
}
if (group.authProvider)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const user = await User.findOne({ id: userId })
if (!user)
throw {
@@ -249,9 +266,17 @@ const updateUsersListInGroup = async (
message: 'User not found.'
}
const updatedGroup = (action === 'addUser'
? await group.addUser(user._id)
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
if (user.authProvider)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const updatedGroup =
action === 'addUser'
? await group.addUser(user)
: await group.removeUser(user)
if (!updatedGroup)
throw {
@@ -260,9 +285,6 @@ const updateUsersListInGroup = async (
message: 'Unable to update group.'
}
if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id)
return {
groupId: updatedGroup.groupId,
name: updatedGroup.name,

View File

@@ -1,9 +1,11 @@
export * from './auth'
export * from './authConfig'
export * from './client'
export * from './code'
export * from './drive'
export * from './group'
export * from './info'
export * from './permission'
export * from './session'
export * from './stp'
export * from './user'

View File

@@ -1,4 +1,8 @@
import { Route, Tags, Example, Get } from 'tsoa'
import { getAuthorizedRoutes } from '../utils'
export interface AuthorizedRoutesResponse {
paths: string[]
}
export interface InfoResponse {
mode: string
@@ -36,4 +40,19 @@ export class InfoController {
}
return response
}
/**
* @summary Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.
*
*/
@Example<AuthorizedRoutesResponse>({
paths: ['/AppStream', '/SASjsApi/stp/execute']
})
@Get('/authorizedRoutes')
public authorizedRoutes(): AuthorizedRoutesResponse {
const response = {
paths: getAuthorizedRoutes()
}
return response
}
}

View File

@@ -1,12 +1,8 @@
import path from 'path'
import fs from 'fs'
import {
getSASSessionController,
getJSSessionController,
processProgram
} from './'
import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types'
import { PreProgramVars, Session, TreeNode, SessionState } from '../../types'
import {
extractHeaders,
getFilesFolder,
@@ -24,12 +20,6 @@ export interface ExecuteReturnRaw {
result: string | Buffer
}
export interface ExecuteReturnJson {
httpHeaders: HTTPHeaders
webout: string | Buffer
log?: string
}
interface ExecuteFileParams {
programPath: string
preProgramVariables: PreProgramVars
@@ -38,10 +28,12 @@ interface ExecuteFileParams {
returnJson?: boolean
session?: Session
runTime: RunTimeType
forceStringResult?: boolean
}
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string
includePrintOutput?: boolean
}
export class ExecutionController {
@@ -52,7 +44,8 @@ export class ExecutionController {
otherArgs,
returnJson,
session,
runTime
runTime,
forceStringResult
}: ExecuteFileParams) {
const program = await readFile(programPath)
@@ -63,7 +56,8 @@ export class ExecutionController {
otherArgs,
returnJson,
session,
runTime
runTime,
forceStringResult
})
}
@@ -72,19 +66,16 @@ export class ExecutionController {
preProgramVariables,
vars,
otherArgs,
returnJson,
session: sessionByFileUpload,
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
runTime,
forceStringResult,
includePrintOutput
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime)
const session =
sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true
session.consumed = true
session.state = SessionState.running
const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt')
@@ -103,6 +94,7 @@ export class ExecutionController {
vars,
session,
weboutPath,
headersPath,
tokenFile,
runTime,
logPath,
@@ -114,33 +106,46 @@ export class ExecutionController {
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') &&
!returnJson && // not a POST Request
!isDebugOn(vars) // Debug is not enabled
if (isDebugOn(vars)) {
httpHeaders['content-type'] = 'text/plain'
}
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
const webout = (await fileExists(weboutPath))
? fileResponse
? fileResponse && !forceStringResult
? await readFileBinary(weboutPath)
: await readFile(weboutPath)
: ''
// it should be deleted by scheduleSessionDestroy
session.inUse = false
session.state = SessionState.completed
if (returnJson) {
return {
httpHeaders,
webout,
log: isDebugOn(vars) || session.crashed ? log : undefined
}
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
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
isDebugOn(vars) || session.failureReason
? resultParts.join(`\n`)
: webout
}
}
@@ -150,6 +155,7 @@ export class ExecutionController {
name: 'files',
relativePath: '',
absolutePath: getFilesFolder(),
isFolder: true,
children: []
}
@@ -159,15 +165,22 @@ export class ExecutionController {
const currentNode = stack.pop()
if (currentNode) {
currentNode.isFolder = fs
.statSync(currentNode.absolutePath)
.isDirectory()
const children = fs.readdirSync(currentNode.absolutePath)
for (let child of children) {
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
const absoluteChildPath = path.join(currentNode.absolutePath, child)
// relative path will only be used in frontend component
// so, no need to convert '/' to platform specific separator
const relativeChildPath = `${currentNode.relativePath}/${child}`
const childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
isFolder: false,
children: []
}
currentNode.children.push(childNode)

View File

@@ -1,12 +1,9 @@
import { Request, RequestHandler } from 'express'
import multer from 'multer'
import { uuidv4 } from '@sasjs/utils'
import { getSASSessionController, getJSSessionController } from '.'
import {
executeProgramRawValidation,
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
import { getSessionController } from '.'
import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils'
import { SessionState } from '../../types'
export class FileUploadController {
private storage = multer.diskStorage({
@@ -37,22 +34,27 @@ export class FileUploadController {
try {
;({ runTime } = await getRunTimeAndFilePath(programPath))
} catch (err: any) {
res.status(400).send({
return res.status(400).send({
status: 'failure',
message: 'Job execution failed',
error: typeof err === 'object' ? err.toString() : err
})
}
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
let sessionController
try {
sessionController = getSessionController(runTime)
} catch (err: any) {
return res.status(400).send({
status: 'failure',
message: err.message,
error: typeof err === 'object' ? err.toString() : err
})
}
const session = await sessionController.getSession()
// marking consumed true, so that it's not available
// as readySession for any other request
session.consumed = true
// change session state to 'running', so that it's not available for any other request
session.state = SessionState.running
req.sasjsSession = session

View File

@@ -1,11 +1,13 @@
import path from 'path'
import { Session } from '../../types'
import { Session, SessionState } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getPackagesFolder,
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath
sysInitCompiledPath,
RunTimeType
} from '../../utils'
import {
deleteFolder,
@@ -17,13 +19,42 @@ import {
const execFilePromise = promisify(execFile)
abstract class SessionController {
export class SessionController {
protected sessions: Session[] = []
protected getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
this.sessions.filter(
(session: Session) => session.state === SessionState.pending
)
protected abstract createSession(): Promise<Session>
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
const session: Session = {
id: sessionId,
state: SessionState.pending,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
this.sessions.push(session)
return session
}
public async getSession() {
const readySessions = this.getReadySessions()
@@ -36,6 +67,10 @@ abstract class SessionController {
return session
}
public getSessionById(id: string) {
return this.sessions.find((session) => session.id === id)
}
}
export class SASSessionController extends SessionController {
@@ -53,15 +88,15 @@ export class SASSessionController extends SessionController {
const session: Session = {
id: sessionId,
ready: false,
inUse: false,
consumed: false,
completed: false,
state: SessionState.initialising,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8\n')
// we do not want to leave sessions running forever
// we clean them up after a predefined period, if unused
this.scheduleSessionDestroy(session)
@@ -71,7 +106,8 @@ export class SASSessionController extends SessionController {
// the autoexec file is executed on SAS startup
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
const contentForAutoExec = `/* compiled systemInit */
const contentForAutoExec = `filename packages "${getPackagesFolder()}";
/* compiled systemInit */
${compiledSystemInitContent}
/* autoexec */
${autoExecContent}`
@@ -88,7 +124,7 @@ ${autoExecContent}`
// Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc, [
execFilePromise(process.sasLoc!, [
'-SYSIN',
codePath,
'-LOG',
@@ -99,20 +135,31 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
'-ENCODING',
'UTF-8',
process.platform === 'win32' ? '-nosplash' : '',
process.platform === 'win32' ? '-icon' : '',
process.platform === 'win32' ? '-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' : '',
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
process.sasLoc!.endsWith('sas.exe') ? '-NOPRNGETLIST' : '',
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
])
.then(() => {
session.completed = true
console.log('session completed', session)
session.state = SessionState.completed
process.logger.info('session completed', session)
})
.catch((err) => {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
session.state = SessionState.failed
session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
})
// we have a triggered session - add to array
@@ -129,15 +176,22 @@ ${autoExecContent}`
const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {}
while (
(await fileExists(codeFilePath)) &&
session.state !== SessionState.failed
) {}
if (session.crashed)
console.log('session crashed! while waiting to be ready', session.crashed)
session.ready = true
if (session.state === SessionState.failed) {
process.logger.error(
'session crashed! while waiting to be ready',
session.failureReason
)
} else {
session.state = SessionState.pending
}
}
public async deleteSession(session: Session) {
private async deleteSession(session: Session) {
// remove the temporary files, to avoid buildup
await deleteFolder(session.path)
@@ -148,66 +202,54 @@ ${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.state === SessionState.running) {
// 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 && session.state !== SessionState.completed) {
// calculate session death time using expiresAfterMins
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) +
expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}
},
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
)
}
}
export class JSSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
export const getSessionController = (
runTime: RunTimeType
): SessionController => {
if (runTime === RunTimeType.SAS) {
process.sasSessionController =
process.sasSessionController || new SASSessionController()
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
const session: Session = {
id: sessionId,
ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: application/json')
this.sessions.push(session)
return session
return process.sasSessionController
}
}
export const getSASSessionController = (): SASSessionController => {
if (process.sasSessionController) return process.sasSessionController
process.sessionController =
process.sessionController || new SessionController()
process.sasSessionController = new SASSessionController()
return process.sasSessionController
}
export const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController()
return process.jsSessionController
return process.sessionController
}
const autoExecContent = `
@@ -218,9 +260,16 @@ data _null_;
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);
/* now wait for the real SYSIN */
slept=0;
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
/* now wait for the real SYSIN (location of code.sas) */
slept=0;fname='';
do until (slept>(60*15));
rc=filename(fname,getoption('SYSIN'));
if rc = 0 and fexist(fname) then do;
putlog fname=;
rc=filename(fname);
rc=sleep(0.01,1); /* wait just a little more */
stop;
end;
slept=slept+sleep(0.01,1);
end;
stop;

View File

@@ -1,3 +1,4 @@
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './'
@@ -8,29 +9,27 @@ export const createJSProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) =>
`${computed}const ${key} = '${vars[key]}';\n`,
`${computed}const ${key} = \`${vars[key]}\`;\n`,
''
)
const preProgramVarStatments = `
let _webout = '';
const weboutPath = '${
process.platform === 'win32'
? weboutPath.replace(/\\/g, '\\\\')
: weboutPath
}';
const _sasjs_tokenfile = '${tokenFile}';
const _sasjs_username = '${preProgramVariables?.username}';
const _sasjs_userid = '${preProgramVariables?.userId}';
const _sasjs_displayname = '${preProgramVariables?.displayName}';
const _metaperson = _sasjs_displayname;
const _metauser = _sasjs_username;
const sasjsprocessmode = 'Stored Program';
const weboutPath = '${escapeWinSlashes(weboutPath)}';
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
const _SASJS_USERNAME = '${preProgramVariables?.username}';
const _SASJS_USERID = '${preProgramVariables?.userId}';
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
const _METAPERSON = _SASJS_DISPLAYNAME;
const _METAUSER = _SASJS_USERNAME;
const SASJSPROCESSMODE = 'Stored Program';
`
const requiredModules = `const fs = require('fs')`
@@ -54,14 +53,15 @@ if (_webout) {
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadJSCode = await generateFileUploadJSCode(
const uploadJsCode = await generateFileUploadJSCode(
otherArgs.filesNamesMap,
session.path
)
//If js code for the file is generated it will be appended to the top of jsCode
if (uploadJSCode.length > 0) {
program = `${uploadJSCode}\n` + program
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadJsCode.length > 0) {
program = `${uploadJsCode}\n` + program
}
}
return requiredModules + program

View File

@@ -0,0 +1,64 @@
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadPythonCode } from '../../utils'
import { ExecutionVars } from './'
export const createPythonProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => `${computed}${key} = '${vars[key]}';\n`,
''
)
const preProgramVarStatments = `
_SASJS_SESSION_PATH = '${escapeWinSlashes(session.path)}';
_WEBOUT = '${escapeWinSlashes(weboutPath)}';
_SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
_SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
_SASJS_USERNAME = '${preProgramVariables?.username}';
_SASJS_USERID = '${preProgramVariables?.userId}';
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
_METAPERSON = _SASJS_DISPLAYNAME;
_METAUSER = _SASJS_USERNAME;
SASJSPROCESSMODE = 'Stored Program';
`
const requiredModules = `import os`
program = `
# runtime vars
${varStatments}
# dynamic user-provided vars
${preProgramVarStatments}
# change working directory to session folder
os.chdir(_SASJS_SESSION_PATH)
# actual job code
${program}
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadPythonCode = await generateFileUploadPythonCode(
otherArgs.filesNamesMap,
session.path
)
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadPythonCode.length > 0) {
program = `${uploadPythonCode}\n` + program
}
}
return requiredModules + program
}

View File

@@ -0,0 +1,64 @@
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadRCode } from '../../utils'
import { ExecutionVars } from '.'
export const createRProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => `${computed}.${key} <- '${vars[key]}'\n`,
''
)
const preProgramVarStatments = `
._SASJS_SESSION_PATH <- '${escapeWinSlashes(session.path)}';
._WEBOUT <- '${escapeWinSlashes(weboutPath)}';
._SASJS_WEBOUT_HEADERS <- '${escapeWinSlashes(headersPath)}';
._SASJS_TOKENFILE <- '${escapeWinSlashes(tokenFile)}';
._SASJS_USERNAME <- '${preProgramVariables?.username}';
._SASJS_USERID <- '${preProgramVariables?.userId}';
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
._METAPERSON <- ._SASJS_DISPLAYNAME;
._METAUSER <- ._SASJS_USERNAME;
SASJSPROCESSMODE <- 'Stored Program';
`
const requiredModules = ``
program = `
# runtime vars
${varStatments}
# dynamic user-provided vars
${preProgramVarStatments}
# change working directory to session folder
setwd(._SASJS_SESSION_PATH)
# actual job code
${program}
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadRCode = await generateFileUploadRCode(
otherArgs.filesNamesMap,
session.path
)
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadRCode.length > 0) {
program = `${uploadRCode}\n` + program
}
}
return requiredModules + program
}

View File

@@ -8,6 +8,7 @@ export const createSASProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
@@ -23,10 +24,14 @@ export const createSASProgram = async (
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _sasjs_webout_headers=${headersPath};
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
/* the below is here for compatibility and will be removed in a future release */
%let sasjs_stpsrv_header_loc=&_sasjs_webout_headers;
%let sasjsprocessmode=Stored Program;
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
%macro _sasjs_server_init();
@@ -34,6 +39,7 @@ export const createSASProgram = async (
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
%mend;
%_sasjs_server_init()
`
program = `
@@ -60,7 +66,8 @@ ${program}`
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}

View File

@@ -4,4 +4,6 @@ export * from './Execution'
export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './createPythonProgram'
export * from './createRProgram'
export * from './processProgram'

View File

@@ -1,11 +1,17 @@
import path from 'path'
import fs from 'fs'
import { execFileSync } from 'child_process'
import { WriteStream, createWriteStream } from 'fs'
import { execFile } from 'child_process'
import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { PreProgramVars, Session, SessionState } from '../../types'
import { RunTimeType } from '../../utils'
import { ExecutionVars, createSASProgram, createJSProgram } from './'
import {
ExecutionVars,
createSASProgram,
createJSProgram,
createPythonProgram,
createRProgram
} from './'
export const processProgram = async (
program: string,
@@ -13,54 +19,20 @@ export const processProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
runTime: RunTimeType,
logPath: string,
otherArgs?: any
) => {
if (runTime === RunTimeType.JS) {
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.js')
try {
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = fs.createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
execFileSync(process.nodeLoc, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.js program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else {
if (runTime === RunTimeType.SAS) {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
@@ -77,10 +49,121 @@ export const processProgram = async (
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status
while (!session.completed) {
while (session.state !== SessionState.completed) {
await delay(50)
}
} else {
let codePath: string
let executablePath: string
switch (runTime) {
case RunTimeType.JS:
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
codePath = path.join(session.path, 'code.js')
executablePath = process.nodeLoc!
break
case RunTimeType.PY:
program = await createPythonProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
codePath = path.join(session.path, 'code.py')
executablePath = process.pythonLoc!
break
case RunTimeType.R:
program = await createRProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
codePath = path.join(session.path, 'code.r')
executablePath = process.rLoc!
break
default:
throw new Error('Invalid runtime!')
}
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
await execFilePromise(executablePath, [codePath], writeStream)
.then(() => {
session.state = SessionState.completed
process.logger.info('session completed', session)
})
.catch((err) => {
session.state = SessionState.failed
session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
})
// copy the code file to log and end write stream
writeStream.end(program)
}
}
/**
* Promisified child_process.execFile
*
* @param file - The name or path of the executable file to run.
* @param args - List of string arguments.
* @param writeStream - Child process stdout and stderr will be piped to it.
*
* @returns {Promise<{ stdout: string, stderr: string }>}
*/
const execFilePromise = (
file: string,
args: string[],
writeStream: WriteStream
): Promise<{ stdout: string; stderr: string }> => {
return new Promise((resolve, reject) => {
const child = execFile(file, args, (err, stdout, stderr) => {
if (err) reject(err)
resolve({ stdout, stderr })
})
child.stdout?.on('data', (data) => {
writeStream.write(data)
})
child.stderr?.on('data', (data) => {
writeStream.write(data)
})
})
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -0,0 +1,283 @@
import { readFile } from '@sasjs/utils'
import express from 'express'
import path from 'path'
import { Request, Post, Get } from 'tsoa'
import dotenv from 'dotenv'
import { ExecutionController } from './internal'
import {
getPreProgramVariables,
getRunTimeAndFilePath,
makeFilesNamesMap
} from '../utils'
import { MulterFile } from '../types/Upload'
dotenv.config()
export interface Sas9Response {
content: string
redirect?: string
error?: boolean
}
export interface MockFileRead {
content: string
error?: boolean
}
export class MockSas9Controller {
private loggedIn: string | undefined
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
@Get('/SASStoredProcess')
public async sasStoredProcess(
@Request() req: express.Request
): Promise<Sas9Response> {
const username = req.query._username?.toString() || undefined
const password = req.query._password?.toString() || undefined
if (username && password) this.loggedIn = req.body.username
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
let program = req.query._program?.toString() || undefined
const filePath: string[] = program
? program.replace('/', '').split('/')
: ['generic', 'sas-stored-process']
if (program) {
return await getMockResponseFromFile([
process.cwd(),
this.mocksPath,
'sas9',
...filePath
])
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
...filePath
])
}
@Get('/SASStoredProcess/do')
public async sasStoredProcessDoGet(
@Request() req: express.Request
): Promise<Sas9Response> {
const username = req.query._username?.toString() || undefined
const password = req.query._password?.toString() || undefined
if (username && password) this.loggedIn = username
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
const program = req.query._program ?? req.body?._program
const filePath: string[] = ['generic', 'sas-stored-process']
if (program) {
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
const otherArgs = {}
try {
const { codePath, runTime } = await getRunTimeAndFilePath(
program + '.js'
)
const result = await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: vars,
otherArgs: otherArgs,
runTime,
forceStringResult: true
})
return {
content: result.result as string
}
} catch (err) {
process.logger.error('err', err)
}
return {
content: 'No webout returned.'
}
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
...filePath
])
}
@Post('/SASStoredProcess/do/')
public async sasStoredProcessDoPost(
@Request() req: express.Request
): Promise<Sas9Response> {
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
if (this.isPublicAccount()) {
return {
content: '',
redirect: '/SASLogon/Login'
}
}
const program = req.query._program ?? req.body?._program
const vars = {
...req.query,
...req.body,
_requestMethod: req.method,
_driveLoc: process.driveLoc
}
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
const otherArgs = { filesNamesMap: filesNamesMap }
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
try {
const result = await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: vars,
otherArgs: otherArgs,
runTime,
session: req.sasjsSession,
forceStringResult: true
})
return {
content: result.result as string
}
} catch (err) {
process.logger.error('err', err)
}
return {
content: 'No webout returned.'
}
}
@Get('/SASLogon/login')
public async loginGet(): Promise<Sas9Response> {
if (this.loggedIn) {
if (this.isPublicAccount()) {
return {
content: '',
redirect: '/SASStoredProcess/Logoff?publicDenied=true'
}
} else {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'logged-in'
])
}
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'login'
])
}
@Post('/SASLogon/login')
public async loginPost(req: express.Request): Promise<Sas9Response> {
if (req.body.lt && req.body.lt !== 'validtoken')
return {
content: '',
redirect: '/SASLogon/login'
}
this.loggedIn = req.body.username
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'logged-in'
])
}
@Get('/SASLogon/logout')
public async logout(req: express.Request): Promise<Sas9Response> {
this.loggedIn = undefined
if (req.query.publicDenied === 'true') {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'public-access-denied'
])
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'logged-out'
])
}
@Get('/SASStoredProcess/Logoff') //publicDenied=true
public async logoff(req: express.Request): Promise<Sas9Response> {
const params = req.query.publicDenied
? `?publicDenied=${req.query.publicDenied}`
: ''
return {
content: '',
redirect: '/SASLogon/logout' + params
}
}
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
}
const getMockResponseFromFile = async (
filePath: string[]
): Promise<MockFileRead> => {
const filePathParsed = path.join(...filePath)
let error: boolean = false
let file = await readFile(filePathParsed).catch((err: any) => {
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
process.logger.error(errMsg)
error = true
return errMsg
})
return {
content: file,
error: error
}
}

View File

@@ -0,0 +1,368 @@
import express from 'express'
import {
Security,
Route,
Tags,
Path,
Example,
Get,
Post,
Patch,
Delete,
Body,
Request
} from 'tsoa'
import Permission from '../model/Permission'
import User from '../model/User'
import Group from '../model/Group'
import { UserResponse } from './user'
import { GroupDetailsResponse } from './group'
export enum PermissionType {
route = 'Route'
}
export enum PrincipalType {
user = 'user',
group = 'group'
}
export enum PermissionSettingForRoute {
grant = 'Grant',
deny = 'Deny'
}
interface RegisterPermissionPayload {
/**
* Name of affected resource
* @example "/SASjsApi/code/execute"
*/
path: string
/**
* Type of affected resource
* @example "Route"
*/
type: PermissionType
/**
* The indication of whether (and to what extent) access is provided
* @example "Grant"
*/
setting: PermissionSettingForRoute
/**
* Indicates the type of principal
* @example "user"
*/
principalType: PrincipalType
/**
* The id of user or group to which a rule is assigned.
* @example 123
*/
principalId: number
}
interface UpdatePermissionPayload {
/**
* The indication of whether (and to what extent) access is provided
* @example "Grant"
*/
setting: PermissionSettingForRoute
}
export interface PermissionDetailsResponse {
permissionId: number
path: string
type: string
setting: string
user?: UserResponse
group?: GroupDetailsResponse
}
@Security('bearerAuth')
@Route('SASjsApi/permission')
@Tags('Permission')
export class PermissionController {
/**
* Get the list of permission rules applicable the authenticated user.
* If 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.
*
*/
@Example<PermissionDetailsResponse[]>([
{
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: []
}
}
])
@Get('/')
public async getAllPermissions(
@Request() request: express.Request
): Promise<PermissionDetailsResponse[]> {
return getAllPermissions(request)
}
/**
* @summary Create a new permission. Admin only.
*
*/
@Example<PermissionDetailsResponse>({
permissionId: 123,
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
id: 1,
username: 'johnSnow01',
displayName: 'John Snow',
isAdmin: false
}
})
@Post('/')
public async createPermission(
@Body() body: RegisterPermissionPayload
): Promise<PermissionDetailsResponse> {
return createPermission(body)
}
/**
* @summary Update permission setting. Admin only
* @param permissionId The permission's identifier
* @example permissionId 1234
*/
@Example<PermissionDetailsResponse>({
permissionId: 123,
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
id: 1,
username: 'johnSnow01',
displayName: 'John Snow',
isAdmin: false
}
})
@Patch('{permissionId}')
public async updatePermission(
@Path() permissionId: number,
@Body() body: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> {
return updatePermission(permissionId, body)
}
/**
* @summary Delete a permission. Admin only.
* @param permissionId The user's identifier
* @example permissionId 1234
*/
@Delete('{permissionId}')
public async deletePermission(@Path() permissionId: number) {
return deletePermission(permissionId)
}
}
const getAllPermissions = async (
req: express.Request
): Promise<PermissionDetailsResponse[]> => {
const { user } = req
if (user?.isAdmin) return await Permission.get({})
else {
const permissions: PermissionDetailsResponse[] = []
const dbUser = await User.findOne({ id: user?.userId })
if (!dbUser)
throw {
code: 404,
status: 'Not Found',
message: 'User not found.'
}
permissions.push(...(await Permission.get({ user: dbUser._id })))
for (const group of dbUser.groups) {
permissions.push(...(await Permission.get({ group })))
}
return permissions
}
}
const createPermission = async ({
path,
type,
setting,
principalType,
principalId
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
const permission = new Permission({
path,
type,
setting
})
let user: UserResponse | undefined
let group: GroupDetailsResponse | undefined
switch (principalType) {
case PrincipalType.user: {
const userInDB = await User.findOne({ id: principalId })
if (!userInDB)
throw {
code: 404,
status: 'Not Found',
message: 'User not found.'
}
if (userInDB.isAdmin)
throw {
code: 400,
status: 'Bad Request',
message: 'Can not add permission for admin user.'
}
const alreadyExists = await Permission.findOne({
path,
type,
user: userInDB._id
})
if (alreadyExists)
throw {
code: 409,
status: 'Conflict',
message:
'Permission already exists with provided Path, Type and User.'
}
permission.user = userInDB._id
user = {
id: userInDB.id,
username: userInDB.username,
displayName: userInDB.displayName,
isAdmin: userInDB.isAdmin
}
break
}
case PrincipalType.group: {
const groupInDB = await Group.findOne({ groupId: principalId })
if (!groupInDB)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
const alreadyExists = await Permission.findOne({
path,
type,
group: groupInDB._id
})
if (alreadyExists)
throw {
code: 409,
status: 'Conflict',
message:
'Permission already exists with provided Path, Type and Group.'
}
permission.group = groupInDB._id
group = {
groupId: groupInDB.groupId,
name: groupInDB.name,
description: groupInDB.description,
isActive: groupInDB.isActive,
users: groupInDB.populate({
path: 'users',
select: 'id username displayName isAdmin -_id',
options: { limit: 15 }
}) as unknown as UserResponse[]
}
break
}
default:
throw {
code: 400,
status: 'Bad Request',
message: 'Invalid principal type. Valid types are user or group.'
}
}
const savedPermission = await permission.save()
return {
permissionId: savedPermission.permissionId,
path: savedPermission.path,
type: savedPermission.type,
setting: savedPermission.setting,
user,
group
}
}
const updatePermission = async (
id: number,
data: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> => {
const { setting } = data
const updatedPermission = (await Permission.findOneAndUpdate(
{ permissionId: id },
{ setting },
{ new: true }
)
.select({
_id: 0,
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({
path: 'group',
select: 'groupId name description -_id'
})) as unknown as PermissionDetailsResponse
if (!updatedPermission)
throw {
code: 404,
status: 'Not Found',
message: 'Permission not found.'
}
return updatedPermission
}
const deletePermission = async (id: number) => {
const permission = await Permission.findOne({ permissionId: id })
if (!permission)
throw {
code: 404,
status: 'Not Found',
message: 'Permission not found.'
}
await Permission.deleteOne({ permissionId: id })
}

View File

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

View File

@@ -1,33 +1,18 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import {
Request,
Security,
Route,
Tags,
Post,
Body,
Get,
Query,
Example
} from 'tsoa'
import {
ExecuteReturnJson,
ExecuteReturnRaw,
ExecutionController,
ExecutionVars
ExecutionVars,
getSessionController
} from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray,
getRunTimeAndFilePath
} from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload {
interface ExecutePostRequestPayload {
/**
* Location of SAS program
* @example "/Public/somefolder/some.file"
@@ -35,15 +20,34 @@ interface ExecuteReturnJsonPayload {
_program?: string
}
interface IRecordOfAny {
[key: string]: any
interface TriggerProgramPayload {
/**
* Location of SAS program.
* @example "/Public/somefolder/some.file"
*/
_program: string
/**
* Amount of minutes after the completion of the program when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
/**
* Query param for setting debug mode.
*/
_debug?: number
}
export interface ExecuteReturnJsonResponse {
status: string
_webout: string | IRecordOfAny
log: LogLine[]
message?: string
httpHeaders: HTTPHeaders
interface TriggerProgramResponse {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store program outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth')
@@ -51,86 +55,106 @@ export interface ExecuteReturnJsonResponse {
@Tags('STP')
export class STPController {
/**
* Trigger a SAS or JS program using the _program URL parameter.
* Trigger a Stored Program using the _program URL parameter.
*
* Accepts URL parameters and file uploads. For more details, see docs:
* Accepts additional URL parameters (converted to session variables)
* and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* @summary Execute a Stored Program, returns raw _webout content.
* @param _program Location of SAS or JS code
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @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 executeReturnRaw(
public async executeGetRequest(
@Request() request: express.Request,
@Query() _program: string
@Query() _program: string,
@Query() _debug?: number
): Promise<string | Buffer> {
return executeReturnRaw(request, _program)
let vars = request.query as ExecutionVars
if (_debug) {
vars = {
...vars,
_debug
}
}
return execute(request, _program, vars)
}
/**
* Trigger a SAS or JS program using the _program URL parameter.
* Trigger a Stored Program using the _program URL parameter.
*
* Accepts URL parameters and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* The response will be a JSON object with the following root attributes:
* log, webout, headers.
*
* The webout attribute will be nested JSON ONLY if the response-header
* contains a content-type of application/json AND it is valid JSON.
* Otherwise it will be a stringified version of the webout content.
*
* @summary Execute a Stored Program, return a JSON object
* @param _program Location of SAS or JS code
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive
* @example _program "/Projects/myApp/some/program"
*/
@Example<ExecuteReturnJsonResponse>({
status: 'success',
_webout: 'webout content',
log: [],
httpHeaders: {
'Content-type': 'application/zip',
'Cache-Control': 'public, max-age=1000'
}
})
@Post('/execute')
public async executeReturnJson(
public async executePostRequest(
@Request() request: express.Request,
@Body() body?: ExecuteReturnJsonPayload,
@Body() body?: ExecutePostRequestPayload,
@Query() _program?: string
): Promise<ExecuteReturnJsonResponse> {
): Promise<string | Buffer> {
const program = _program ?? body?._program
return executeReturnJson(request, program!)
const vars = { ...request.query, ...request.body }
const filesNamesMap = request.files?.length
? makeFilesNamesMap(request.files as MulterFile[])
: null
const otherArgs = { filesNamesMap: filesNamesMap }
return execute(request, program!, vars, otherArgs)
}
/**
* Trigger Program on the Specified Runtime.
* @summary Triggers program and returns SessionId immediately - does not wait for program completion.
* @param _program Location of code in SASjs Drive.
* @param expiresAfterMins Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.
* @param _debug Optional query param for setting debug mode.
* @example _program "/Projects/myApp/some/program"
* @example _debug 131
* @example expiresAfterMins 15
*/
@Post('/trigger')
public async triggerProgram(
@Request() request: express.Request,
@Query() _program: string,
@Query() _debug?: number,
@Query() expiresAfterMins?: number
): Promise<TriggerProgramResponse> {
return triggerProgram(request, { _program, _debug, expiresAfterMins })
}
}
const executeReturnRaw = async (
const execute = async (
req: express.Request,
_program: string
_program: string,
vars: ExecutionVars,
otherArgs?: any
): Promise<string | Buffer> => {
const query = req.query as ExecutionVars
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { result, httpHeaders } =
(await new ExecutionController().executeFile({
const { result, httpHeaders } = await new ExecutionController().executeFile(
{
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars: query,
runTime
})) as ExecuteReturnRaw
vars,
otherArgs,
session: req.sasjsSession
}
)
// Should over-ride response header for debug
// on GET request to see entire log rendering on browser.
if (isDebugOn(query)) {
httpHeaders['content-type'] = 'text/plain'
}
req.res?.set(httpHeaders)
req.res?.header(httpHeaders)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
@@ -147,41 +171,45 @@ const executeReturnRaw = async (
}
}
const executeReturnJson = async (
const triggerProgram = async (
req: express.Request,
_program: string
): Promise<ExecuteReturnJsonResponse> => {
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
{ _program, _debug, expiresAfterMins }: TriggerProgramPayload
): Promise<TriggerProgramResponse> => {
try {
// put _program query param into vars object
const vars: { [key: string]: string | number } = { _program }
// if present add _debug query param to vars object
if (_debug) {
vars._debug = _debug
}
// get code path and runTime
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { webout, log, httpHeaders } =
(await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, ...req.body },
otherArgs: { filesNamesMap: filesNamesMap },
returnJson: true,
session: req.sasjsSession,
runTime
})) as ExecuteReturnJson
// get session controller based on runTime
const sessionController = getSessionController(runTime)
let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
try {
weboutRes = JSON.parse(webout as string)
} catch (_) {}
// 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 }
}
return {
status: 'success',
_webout: weboutRes,
log: parseLogToArray(log),
httpHeaders
}
// call executeFile method of ExecutionController without awaiting
new ExecutionController().executeFile({
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars,
session
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,

View File

@@ -17,16 +17,22 @@ import {
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import { GroupResponse } from './group'
import {
getUserAutoExec,
updateUserAutoExec,
ModeType,
ALL_USERS_GROUP
} from '../utils'
import { GroupController, GroupResponse } from './group'
export interface UserResponse {
id: number
username: string
displayName: string
isAdmin: boolean
}
interface UserDetailsResponse {
export interface UserDetailsResponse {
id: number
displayName: string
username: string
@@ -48,12 +54,14 @@ export class UserController {
{
id: 123,
username: 'johnusername',
displayName: 'John'
displayName: 'John',
isAdmin: false
},
{
id: 456,
username: 'starkusername',
displayName: 'Stark'
displayName: 'Stark',
isAdmin: true
}
])
@Get('/')
@@ -200,7 +208,7 @@ export class UserController {
const getAllUsers = async (): Promise<UserResponse[]> =>
await User.find({})
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
.exec()
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
@@ -208,7 +216,11 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
if (usernameExist)
throw {
code: 409,
message: 'Username already exists.'
}
// Hash passwords
const hashPassword = User.hashPassword(password)
@@ -225,6 +237,15 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const savedUser = await user.save()
const groupController = new GroupController()
const allUsersGroup = await groupController
.getGroupByGroupName(ALL_USERS_GROUP.name)
.catch(() => {})
if (allUsersGroup) {
await groupController.addUserToGroup(allUsersGroup.groupId, savedUser.id)
}
return {
id: savedUser.id,
displayName: savedUser.displayName,
@@ -252,7 +273,11 @@ const getUser = async (
'groupId name description -_id'
)) as unknown as UserDetailsResponse
if (!user) throw new Error('User is not found.')
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
return {
id: user.id,
@@ -260,7 +285,7 @@ const getUser = async (
username: user.username,
isActive: user.isActive,
isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
autoExec: getAutoExec ? (user.autoExec ?? '') : undefined,
groups: user.groups
}
}
@@ -281,6 +306,24 @@ const updateUser = async (
const params: any = { displayName, isAdmin, isActive, autoExec }
const user = await User.findOne(findBy)
if (username && username !== user?.username && user?.authProvider) {
throw {
code: 405,
message:
'Can not update username of user that is created by an external auth provider.'
}
}
if (displayName && displayName !== user?.displayName && user?.authProvider) {
throw {
code: 405,
message:
'Can not update display name of user that is created by an external auth provider.'
}
}
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
@@ -289,7 +332,10 @@ const updateUser = async (
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw new Error('Username already exists.')
throw {
code: 409,
message: 'Username already exists.'
}
}
params.username = username
}
@@ -302,7 +348,10 @@ const updateUser = async (
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
if (!updatedUser)
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
throw {
code: 404,
message: `Unable to find user with ${findBy.id || findBy.username}`
}
return {
id: updatedUser.id,
@@ -329,11 +378,19 @@ const deleteUser = async (
{ password }: { password?: string }
) => {
const user = await User.findOne(findBy)
if (!user) throw new Error('User is not found.')
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
if (!isAdmin) {
const validPass = user.comparePassword(password!)
if (!validPass) throw new Error('Invalid password.')
if (!validPass)
throw {
code: 401,
message: 'Invalid password.'
}
}
await User.deleteOne(findBy)

View File

@@ -1,11 +1,17 @@
import path from 'path'
import express from 'express'
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
import { readFile } from '@sasjs/utils'
import { readFile, convertSecondsToHms } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import { getWebBuildFolder, generateAuthCode } from '../utils'
import {
getWebBuildFolder,
generateAuthCode,
RateLimiter,
AuthProviderType,
LDAPClient
} from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@@ -49,10 +55,10 @@ export class WebController {
}
/**
* @summary Accept a valid username/password
* @summary Destroy the session stored in cookies
*
*/
@Get('/logout')
@Get('/SASLogon/logout')
public async logout(@Request() req: express.Request) {
return new Promise((resolve) => {
req.session.destroy(() => {
@@ -78,10 +84,40 @@ const login = async (
) => {
// Authenticate User
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
let validPass = false
if (user) {
if (
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
user.authProvider === AuthProviderType.LDAP
) {
const ldapClient = await LDAPClient.init()
validPass = await ldapClient
.verifyUser(username, password)
.catch(() => false)
} else {
validPass = user.comparePassword(password)
}
}
// code to prevent brute force attack
const rateLimiter = RateLimiter.getInstance()
if (!validPass) {
const retrySecs = await rateLimiter.consume(
req.ip || 'unknown',
user?.username
)
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
}
if (!user) throw errors.userNotFound
if (!validPass) throw errors.invalidPassword
// Reset on successful authorization
rateLimiter.resetOnSuccess(req.ip || 'unknown', user.username)
req.session.loggedIn = true
req.session.user = {
@@ -91,7 +127,8 @@ const login = async (
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive,
autoExec: user.autoExec
autoExec: user.autoExec,
needsToUpdatePassword: user.needsToUpdatePassword
}
return {
@@ -99,7 +136,9 @@ const login = async (
user: {
id: user.id,
username: user.username,
displayName: user.displayName
displayName: user.displayName,
isAdmin: user.isAdmin,
needsToUpdatePassword: user.needsToUpdatePassword
}
}
}
@@ -156,3 +195,18 @@ interface AuthorizeResponse {
*/
code: string
}
const errors = {
invalidPassword: {
code: 401,
message: 'Invalid Password.'
},
userNotFound: {
code: 401,
message: 'Username is not found.'
},
tooManyRequests: (seconds: number) => ({
code: 429,
message: `Too Many Requests! Retry after ${convertSecondsToHms(seconds)}`
})
}

View File

@@ -1,8 +1,16 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
import { csrfProtection } from './'
import {
fetchLatestAutoExec,
ModeType,
verifyTokenInDB,
isAuthorizingRoute,
isPublicRoute,
publicUser
} from '../utils'
import { desktopUser } from './desktop'
import { authorize } from './authorize'
export const authenticateAccessToken: RequestHandler = async (
req,
@@ -15,6 +23,10 @@ export const authenticateAccessToken: RequestHandler = async (
return next()
}
const nextFunction = isAuthorizingRoute(req)
? () => authorize(req, res, next)
: next
// if request is coming from web and has valid session
// it can be validated.
if (req.session?.loggedIn) {
@@ -24,33 +36,37 @@ export const authenticateAccessToken: RequestHandler = async (
if (user) {
if (user.isActive) {
req.user = user
return csrfProtection(req, res, next)
} else return res.sendStatus(401)
return csrfProtection(req, res, nextFunction)
} else return res.status(401).send('Unauthorized')
}
}
return res.sendStatus(401)
return res.status(401).send('Unauthorized')
}
authenticateToken(
await authenticateToken(
req,
res,
next,
process.env.ACCESS_TOKEN_SECRET as string,
nextFunction,
process.secrets.ACCESS_TOKEN_SECRET,
'accessToken'
)
}
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
authenticateToken(
export const authenticateRefreshToken: RequestHandler = async (
req,
res,
next
) => {
await authenticateToken(
req,
res,
next,
process.env.REFRESH_TOKEN_SECRET as string,
process.secrets.REFRESH_TOKEN_SECRET,
'refreshToken'
)
}
const authenticateToken = (
const authenticateToken = async (
req: Request,
res: Response,
next: NextFunction,
@@ -58,14 +74,15 @@ const authenticateToken = (
tokenType: 'accessToken' | 'refreshToken'
) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
if (MODE === ModeType.Desktop) {
req.user = {
userId: 1234,
clientId: 'desktopModeClientId',
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',
isAdmin: true,
isActive: true
isActive: true,
needsToUpdatePassword: false
}
req.accessToken = 'desktopModeAccessToken'
return next()
@@ -73,12 +90,12 @@ const authenticateToken = (
const authHeader = req.headers['authorization']
const token = authHeader?.split(' ')[1]
if (!token) return res.sendStatus(401)
jwt.verify(token, key, async (err: any, data: any) => {
if (err) return res.sendStatus(401)
try {
if (!token) throw 'Unauthorized'
const data: any = jwt.verify(token, key)
// verify this valid token's entry in DB
const user = await verifyTokenInDB(
data?.userId,
data?.clientId,
@@ -91,8 +108,16 @@ const authenticateToken = (
req.user = user
if (tokenType === 'accessToken') req.accessToken = token
return next()
} else return res.sendStatus(401)
} else throw 'Unauthorized'
}
return res.sendStatus(401)
})
throw 'Unauthorized'
} catch (error) {
if (await isPublicRoute(req)) {
req.user = publicUser
return next()
}
res.status(401).send('Unauthorized')
}
}

View File

@@ -0,0 +1,87 @@
import { RequestHandler } from 'express'
import User from '../model/User'
import Permission from '../model/Permission'
import {
PermissionSettingForRoute,
PermissionType
} from '../controllers/permission'
import { getPath, isPublicRoute, TopLevelRoutes } from '../utils'
export const authorize: RequestHandler = async (req, res, next) => {
const { user } = req
if (!user) return res.sendStatus(401)
// no need to check for permissions when user is admin
if (user.isAdmin) return 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 })
if (!dbUser) return res.sendStatus(401)
const path = getPath(req)
const { baseUrl } = req
const topLevelRoute =
TopLevelRoutes.find((route) => baseUrl.startsWith(route)) || baseUrl
// find permission w.r.t user
const permission = await Permission.findOne({
path,
type: PermissionType.route,
user: dbUser._id
})
if (permission) {
if (permission.setting === PermissionSettingForRoute.grant) return next()
else return res.sendStatus(401)
}
// find permission w.r.t user on top level
const topLevelPermission = await Permission.findOne({
path: topLevelRoute,
type: PermissionType.route,
user: dbUser._id
})
if (topLevelPermission) {
if (topLevelPermission.setting === PermissionSettingForRoute.grant)
return next()
else return res.sendStatus(401)
}
let isPermissionDenied = false
// find permission w.r.t user's groups
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({
path,
type: PermissionType.route,
group
})
if (groupPermission) {
if (groupPermission.setting === PermissionSettingForRoute.grant) {
return next()
} else {
isPermissionDenied = true
}
}
}
if (!isPermissionDenied) {
// find permission w.r.t user's groups on top level
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({
path: topLevelRoute,
type: PermissionType.route,
group
})
if (groupPermission?.setting === PermissionSettingForRoute.grant)
return next()
}
}
return res.sendStatus(401)
}

View File

@@ -0,0 +1,22 @@
import { RequestHandler } from 'express'
import { convertSecondsToHms } from '@sasjs/utils'
import { RateLimiter } from '../utils'
export const bruteForceProtection: RequestHandler = async (req, res, next) => {
const ip = req.ip || 'unknown'
const username = req.body.username
const rateLimiter = RateLimiter.getInstance()
const retrySecs = await rateLimiter.check(ip, username)
if (retrySecs > 0) {
res
.status(429)
.send(`Too Many Requests! Retry after ${convertSecondsToHms(retrySecs)}`)
return
}
next()
}

View File

@@ -0,0 +1,32 @@
import { RequestHandler } from 'express'
import csrf from 'csrf'
const csrfTokens = new csrf()
const secret = csrfTokens.secretSync()
export const generateCSRFToken = () => csrfTokens.create(secret)
export const csrfProtection: RequestHandler = (req, res, next) => {
if (req.method === 'GET') return next()
// Reads the token from the following locations, in order:
// req.body.csrf_token - typically generated by the body-parser module.
// req.query.csrf_token - a built-in from Express.js to read from the URL query string.
// req.headers['csrf-token'] - the CSRF-Token HTTP request header.
// req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
// req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
// req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.
const token =
req.body?.csrf_token ||
req.query?.csrf_token ||
req.headers['csrf-token'] ||
req.headers['xsrf-token'] ||
req.headers['x-csrf-token'] ||
req.headers['x-xsrf-token']
if (!csrfTokens.verify(secret, token)) {
return res.status(400).send('Invalid CSRF token!')
}
next()
}

View File

@@ -33,5 +33,6 @@ export const desktopUser: RequestUser = {
username: userInfo().username,
displayName: userInfo().username,
isAdmin: true,
isActive: true
isActive: true,
needsToUpdatePassword: false
}

View File

@@ -1,4 +1,7 @@
export * from './authenticateToken'
export * from './authorize'
export * from './csrfProtection'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'
export * from './bruteForceProtection'

View File

@@ -1,8 +1,9 @@
import { RequestHandler } from 'express'
import { ModeType } from '../utils'
export const verifyAdmin: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') return next()
if (MODE === ModeType.Desktop) return next()
const { user } = req
if (!user?.isAdmin) return res.status(401).send('Admin account required')

View File

@@ -1,5 +1,6 @@
import mongoose, { Schema } from 'mongoose'
export const NUMBER_OF_SECONDS_IN_A_DAY = 86400
export interface ClientPayload {
/**
* Client ID
@@ -11,6 +12,16 @@ export interface ClientPayload {
* @example "someRandomCryptoString"
*/
clientSecret: string
/**
* Number of seconds after which access token will expire. Default is 86400 (1 day)
* @example 86400
*/
accessTokenExpiration?: number
/**
* Number of seconds after which access token will expire. Default is 2592000 (30 days)
* @example 2592000
*/
refreshTokenExpiration?: number
}
const ClientSchema = new Schema<ClientPayload>({
@@ -21,6 +32,14 @@ const ClientSchema = new Schema<ClientPayload>({
clientSecret: {
type: String,
required: true
},
accessTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY
},
refreshTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
})

View File

@@ -0,0 +1,45 @@
import mongoose, { Schema } from 'mongoose'
export interface ConfigurationType {
/**
* SecretOrPrivateKey to sign Access Token
* @example "someRandomCryptoString"
*/
ACCESS_TOKEN_SECRET: string
/**
* SecretOrPrivateKey to sign Refresh Token
* @example "someRandomCryptoString"
*/
REFRESH_TOKEN_SECRET: string
/**
* SecretOrPrivateKey to sign Auth Code
* @example "someRandomCryptoString"
*/
AUTH_CODE_SECRET: string
/**
* Secret used to sign the session cookie
* @example "someRandomCryptoString"
*/
SESSION_SECRET: string
}
const ConfigurationSchema = new Schema<ConfigurationType>({
ACCESS_TOKEN_SECRET: {
type: String,
required: true
},
REFRESH_TOKEN_SECRET: {
type: String,
required: true
},
AUTH_CODE_SECRET: {
type: String,
required: true
},
SESSION_SECRET: {
type: String,
required: true
}
})
export default mongoose.model('Configuration', ConfigurationSchema)

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

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

View File

@@ -1,6 +1,9 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
import User from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import { Schema, model, Document, Model } from 'mongoose'
import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User'
import { AuthProviderType, getSequenceNextValue } from '../utils'
export const PUBLIC_GROUP_NAME = 'Public'
export interface GroupPayload {
/**
@@ -24,11 +27,13 @@ interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean
users: Schema.Types.ObjectId[]
authProvider?: AuthProviderType
}
interface IGroup extends IGroupDocument {
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
addUser(user: IUser): Promise<GroupDetailsResponse>
removeUser(user: IUser): Promise<GroupDetailsResponse>
hasUser(user: IUser): boolean
}
interface IGroupModel extends Model<IGroup> {}
@@ -38,10 +43,18 @@ const groupSchema = new Schema<IGroupDocument>({
required: true,
unique: true
},
groupId: {
type: Number,
unique: true
},
description: {
type: String,
default: 'Group description.'
},
authProvider: {
type: String,
enum: AuthProviderType
},
isActive: {
type: Boolean,
default: true
@@ -49,9 +62,13 @@ const groupSchema = new Schema<IGroupDocument>({
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks
groupSchema.pre('save', async function () {
if (this.isNew) {
this.groupId = await getSequenceNextValue('groupId')
}
})
groupSchema.post('save', function (group: IGroup, next: Function) {
group.populate('users', 'id username displayName -_id').then(function () {
next()
@@ -59,7 +76,7 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
})
// pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function () {
groupSchema.pre('remove', async function (this: IGroupDocument) {
const userIds = this.users
await Promise.all(
userIds.map(async (userId) => {
@@ -70,28 +87,31 @@ groupSchema.pre('remove', async function () {
})
// Instance Methods
groupSchema.method(
'addUser',
async function (userObjectId: Schema.Types.ObjectId) {
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex === -1) {
this.users.push(userObjectId)
}
this.markModified('users')
return this.save()
groupSchema.method('addUser', async function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex === -1) {
this.users.push(userObjectId)
user.addGroup(this._id)
}
)
groupSchema.method(
'removeUser',
async function (userObjectId: Schema.Types.ObjectId) {
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex > -1) {
this.users.splice(userIdIndex, 1)
}
this.markModified('users')
return this.save()
this.markModified('users')
return this.save()
})
groupSchema.method('removeUser', async function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex > -1) {
this.users.splice(userIdIndex, 1)
user.removeGroup(this._id)
}
)
this.markModified('users')
return this.save()
})
groupSchema.method('hasUser', function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
return userIdIndex > -1
})
export const Group: IGroupModel = model<IGroup, IGroupModel>(
'Group',

View File

@@ -0,0 +1,82 @@
import { Schema, model, Document, Model } from 'mongoose'
import { PermissionDetailsResponse } from '../controllers'
import { getSequenceNextValue } from '../utils'
interface GetPermissionBy {
user?: Schema.Types.ObjectId
group?: Schema.Types.ObjectId
}
interface IPermissionDocument extends Document {
path: string
type: string
setting: string
permissionId: number
user: Schema.Types.ObjectId
group: Schema.Types.ObjectId
}
interface IPermission extends IPermissionDocument {}
interface IPermissionModel extends Model<IPermission> {
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
}
const permissionSchema = new Schema<IPermissionDocument>({
permissionId: {
type: Number,
unique: true
},
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' }
})
// Hooks
permissionSchema.pre('save', async function () {
if (this.isNew) {
this.permissionId = await getSequenceNextValue('permissionId')
}
})
// 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' })
.populate({
path: 'group',
select: 'groupId name description -_id',
populate: {
path: 'users',
select: 'id username displayName isAdmin -_id',
options: { limit: 15 }
}
})) as unknown as PermissionDetailsResponse[]
})
export const Permission: IPermissionModel = model<
IPermission,
IPermissionModel
>('Permission', permissionSchema)
export default Permission

View File

@@ -1,6 +1,6 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import { Schema, model, Document, Model } from 'mongoose'
import bcrypt from 'bcryptjs'
import { AuthProviderType, getSequenceNextValue } from '../utils'
export interface UserPayload {
/**
@@ -35,15 +35,18 @@ export interface UserPayload {
}
interface IUserDocument extends UserPayload, Document {
_id: Schema.Types.ObjectId
id: number
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType
}
interface IUser extends IUserDocument {
export interface IUser extends IUserDocument {
comparePassword(password: string): boolean
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
@@ -62,10 +65,18 @@ const userSchema = new Schema<IUserDocument>({
required: true,
unique: true
},
id: {
type: Number,
unique: true
},
password: {
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType
},
isAdmin: {
type: Boolean,
default: false
@@ -74,6 +85,10 @@ const userSchema = new Schema<IUserDocument>({
type: Boolean,
default: true
},
needsToUpdatePassword: {
type: Boolean,
default: true
},
autoExec: {
type: String
},
@@ -95,7 +110,15 @@ const userSchema = new Schema<IUserDocument>({
}
]
})
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
// Hooks
userSchema.pre('save', async function (next) {
if (this.isNew) {
this.id = await getSequenceNextValue('id')
}
next()
})
// Static Methods
userSchema.static('hashPassword', (password: string): string => {

View File

@@ -7,12 +7,28 @@ import {
authenticateRefreshToken
} from '../../middlewares'
import { authorizeValidation, tokenValidation } from '../../utils'
import { tokenValidation, updatePasswordValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()
const controller = new AuthController()
authRouter.patch(
'/updatePassword',
authenticateAccessToken,
async (req, res) => {
const { error, value: body } = updatePasswordValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
await controller.updatePassword(req, body)
res.sendStatus(204)
} catch (err: any) {
res.status(err.code).send(err.message)
}
}
)
authRouter.post('/token', async (req, res) => {
const { error, value: body } = tokenValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)

View File

@@ -0,0 +1,25 @@
import express from 'express'
import { AuthConfigController } from '../../controllers'
const authConfigRouter = express.Router()
authConfigRouter.get('/', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = controller.getDetail()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
authConfigRouter.post('/synchroniseWithLDAP', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = await controller.synchroniseWithLDAP()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
export default authConfigRouter

View File

@@ -1,6 +1,7 @@
import express from 'express'
import { ClientController } from '../../controllers'
import { registerClientValidation } from '../../utils'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
const clientRouter = express.Router()
@@ -17,4 +18,19 @@ clientRouter.post('/', async (req, res) => {
}
})
clientRouter.get(
'/',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const controller = new ClientController()
try {
const response = await controller.getAllClients()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
export default clientRouter

View File

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

View File

@@ -11,8 +11,10 @@ import {
extractName,
fileBodyValidation,
fileParamValidation,
folderBodyValidation,
folderParamValidation,
isZipFile
isZipFile,
renameBodyValidation
} from '../../utils'
const controller = new DriveController()
@@ -119,7 +121,11 @@ driveRouter.get('/file', async (req, res) => {
try {
await controller.getFile(req, query._filePath)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -132,7 +138,11 @@ driveRouter.get('/folder', async (req, res) => {
const response = await controller.getFolder(query._folderPath)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -145,7 +155,28 @@ driveRouter.delete('/file', async (req, res) => {
const response = await controller.deleteFile(query._filePath)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.delete('/folder', async (req, res) => {
const { error: errQ, value: query } = folderParamValidation(req.query, true)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.deleteFolder(query._folderPath)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -172,11 +203,33 @@ driveRouter.post(
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
driveRouter.post('/folder', async (req, res) => {
const { error, value: body } = folderBodyValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.addFolder(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.patch(
'/file',
(...arg) => multerSingle('file', arg),
@@ -200,11 +253,33 @@ driveRouter.patch(
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
driveRouter.post('/rename', async (req, res) => {
const { error, value: body } = renameBodyValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.rename(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.get('/fileTree', async (req, res) => {
try {
const response = await controller.getFileTree()

View File

@@ -18,11 +18,7 @@ groupRouter.post(
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -33,11 +29,7 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -49,11 +41,7 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -71,11 +59,7 @@ groupRouter.get(
const response = await controller.getGroupByGroupName(name)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -95,11 +79,7 @@ groupRouter.post(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -119,11 +99,7 @@ groupRouter.delete(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -140,11 +116,7 @@ groupRouter.delete(
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)

View File

@@ -17,6 +17,8 @@ import groupRouter from './group'
import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
import permissionRouter from './permission'
import authConfigRouter from './authConfig'
const router = express.Router()
@@ -35,6 +37,20 @@ router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter)
router.use(
'/permission',
desktopRestrict,
authenticateAccessToken,
permissionRouter
)
router.use(
'/authConfig',
desktopRestrict,
authenticateAccessToken,
verifyAdmin,
authConfigRouter
)
router.use(
'/',

View File

@@ -13,4 +13,14 @@ infoRouter.get('/', async (req, res) => {
}
})
infoRouter.get('/authorizedRoutes', async (req, res) => {
const controller = new InfoController()
try {
const response = controller.authorizedRoutes()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default infoRouter

View File

@@ -0,0 +1,69 @@
import express from 'express'
import { PermissionController } from '../../controllers/'
import { verifyAdmin } from '../../middlewares'
import {
registerPermissionValidation,
updatePermissionValidation
} from '../../utils'
const permissionRouter = express.Router()
const controller = new PermissionController()
permissionRouter.get('/', async (req, res) => {
try {
const response = await controller.getAllPermissions(req)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
permissionRouter.post('/', verifyAdmin, async (req, res) => {
const { error, value: body } = registerPermissionValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.createPermission(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
const { permissionId } = req.params
const { error, value: body } = updatePermissionValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.updatePermission(permissionId, body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
permissionRouter.delete(
'/:permissionId',
verifyAdmin,
async (req: any, res) => {
const { permissionId } = req.params
try {
await controller.deletePermission(permissionId)
res.status(200).send('Permission Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
export default permissionRouter

View File

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

View File

@@ -5,6 +5,7 @@ import request from 'supertest'
import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../../../model/Client'
const client = {
clientId: 'someclientID',
@@ -26,6 +27,7 @@ describe('client', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
const userController = new UserController()
const clientController = new ClientController()
@@ -34,6 +36,18 @@ describe('client', () => {
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterAll(async () => {
@@ -43,22 +57,6 @@ describe('client', () => {
})
describe('create', () => {
let adminAccessToken: string
beforeAll(async () => {
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
@@ -157,4 +155,80 @@ describe('client', () => {
expect(res.body).toEqual({})
})
})
describe('get', () => {
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
await collection.deleteMany({})
})
it('should respond with an array of all clients', async () => {
await clientController.createClient(newClient)
await clientController.createClient({
clientId: 'clientID',
clientSecret: 'clientSecret'
})
const res = await request(app)
.get('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const expected = [
{
clientId: 'newClientID',
clientSecret: 'newClientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'clientID',
clientSecret: 'clientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
]
expect(res.body).toEqual(expected)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/client').send().expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbideen if access token is not of an admin account', async () => {
const user = {
displayName: 'User 2',
username: 'username2',
password: '12345678',
isAdmin: false,
isActive: true
}
const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
accessToken,
'refreshToken'
)
const res = await request(app)
.get('/SASjsApi/client')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
})
})

View File

@@ -29,7 +29,13 @@ jest
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import {
UserController,
PermissionController,
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../../../controllers/'
import { getTreeExample } from '../../../controllers/internal'
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getFilesFolder } = fileUtilModules
@@ -43,11 +49,18 @@ const user = {
isActive: true
}
const permission = {
type: PermissionType.route,
principalType: PrincipalType.user,
setting: PermissionSettingForRoute.grant
}
describe('drive', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const controller = new UserController()
const permissionController = new PermissionController()
let accessToken: string
@@ -58,11 +71,32 @@ describe('drive', () => {
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await controller.createUser(user)
accessToken = generateAccessToken({
clientId,
userId: dbUser.id
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/deploy',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/deploy/upload',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/file',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/folder',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/rename',
principalId: dbUser.id
})
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
})
afterAll(async () => {
@@ -517,29 +551,29 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folder is not present', async () => {
it('should respond with Not Found if folder is not present', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
.expect(403)
.expect(404)
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
expect(res.text).toEqual(`Folder doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath outside Drive', async () => {
it('should respond with Bad Request if folderPath outside Drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: '/../path/code.sas' })
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
expect(res.text).toEqual(`Can't get folder outside drive.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath is of a file', async () => {
it('should respond with Bad Request if folderPath is of a file', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const filePath = '/my/path/code.sas'
@@ -550,12 +584,96 @@ describe('drive', () => {
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: filePath })
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Not a Folder.')
expect(res.text).toEqual('Not a Folder.')
expect(res.body).toEqual({})
})
})
describe('post', () => {
const folderApi = '/SASjsApi/drive/folder'
const pathToDrive = fileUtilModules.getFilesFolder()
afterEach(async () => {
await deleteFolder(path.join(pathToDrive, 'post'))
})
it('should create a folder on drive', async () => {
const res = await request(app)
.post(folderApi)
.auth(accessToken, { type: 'bearer' })
.send({ folderPath: '/post/folder' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Conflict if the folder already exists', async () => {
await createFolder(path.join(pathToDrive, '/post/folder'))
const res = await request(app)
.post(folderApi)
.auth(accessToken, { type: 'bearer' })
.send({ folderPath: '/post/folder' })
.expect(409)
expect(res.text).toEqual(`Folder already exists.`)
expect(res.statusCode).toEqual(409)
})
it('should respond with Bad Request if the folderPath is outside drive', async () => {
const res = await request(app)
.post(folderApi)
.auth(accessToken, { type: 'bearer' })
.send({ folderPath: '../sample' })
.expect(400)
expect(res.text).toEqual(`Can't put folder outside drive.`)
})
})
describe('delete', () => {
const folderApi = '/SASjsApi/drive/folder'
const pathToDrive = fileUtilModules.getFilesFolder()
it('should delete a folder on drive', async () => {
await createFolder(path.join(pathToDrive, 'delete'))
const res = await request(app)
.delete(folderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: 'delete' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Not Found if the folder does not exists', async () => {
const res = await request(app)
.delete(folderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: 'notExists' })
.expect(404)
expect(res.text).toEqual(`Folder doesn't exist.`)
})
it('should respond with Bad Request if the folderPath is outside drive', async () => {
const res = await request(app)
.delete(folderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: '../outsideDrive' })
.expect(400)
expect(res.text).toEqual(`Can't delete folder outside drive.`)
})
})
})
describe('file', () => {
@@ -601,7 +719,7 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is already present', async () => {
it('should respond with Conflict if file is already present', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
@@ -616,13 +734,13 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: File already exists.')
expect(res.text).toEqual('File already exists.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
it('should respond with Bad Request if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
@@ -631,9 +749,9 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot put file outside drive.')
expect(res.text).toEqual(`Can't put file outside drive.`)
expect(res.body).toEqual({})
})
@@ -768,19 +886,19 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is not present', async () => {
it('should respond with Not Found if file is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', `/my/path/code-3.sas`)
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(403)
.expect(404)
expect(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.text).toEqual(`File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
it('should respond with Bad Request if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
@@ -789,9 +907,9 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
expect(res.text).toEqual(`Can't modify file outside drive.`)
expect(res.body).toEqual({})
})
@@ -896,25 +1014,25 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is not present', async () => {
it('should respond with Not Found if file is not present', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: `/my/path/code-4.sas` })
.expect(403)
.expect(404)
expect(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.text).toEqual(`File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
it('should respond with Bad Request if filePath outside Drive', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/../path/code.sas' })
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot get file outside drive.')
expect(res.text).toEqual(`Can't get file outside drive.`)
expect(res.body).toEqual({})
})
@@ -940,8 +1058,150 @@ describe('drive', () => {
})
})
})
describe('rename', () => {
const renameApi = '/SASjsApi/drive/rename'
const pathToDrive = fileUtilModules.getFilesFolder()
afterEach(async () => {
await deleteFolder(path.join(pathToDrive, 'rename'))
})
it('should rename a folder', async () => {
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/folder', newPath: '/rename/renamed' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should rename a file', async () => {
await createFile(
path.join(pathToDrive, 'rename', 'file.txt'),
'some file content'
)
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({
oldPath: '/rename/file.txt',
newPath: '/rename/renamed.txt'
})
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Bad Request if the oldPath is missing', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ newPath: 'newPath' })
.expect(400)
expect(res.text).toEqual(`\"oldPath\" is required`)
})
it('should respond with Bad Request if the newPath is missing', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: 'oldPath' })
.expect(400)
expect(res.text).toEqual(`\"newPath\" is required`)
})
it('should respond with Bad Request if the oldPath is outside drive', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '../outside', newPath: 'renamed' })
.expect(400)
expect(res.text).toEqual(`Old path can't be outside of drive.`)
})
it('should respond with Bad Request if the newPath is outside drive', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: 'older', newPath: '../outside' })
.expect(400)
expect(res.text).toEqual(`New path can't be outside of drive.`)
})
it('should respond with Not Found if the folder does not exist', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' })
.expect(404)
expect(res.text).toEqual('No file/folder found for provided path.')
})
it('should respond with Conflict if the folder already exists', async () => {
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
await createFolder(path.join(pathToDrive, 'rename', 'exists'))
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/folder', newPath: '/rename/exists' })
.expect(409)
expect(res.text).toEqual('Folder with new name already exists.')
})
it('should respond with Not Found if the file does not exist', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' })
.expect(404)
expect(res.text).toEqual('No file/folder found for provided path.')
})
it('should respond with Conflict if the file already exists', async () => {
await createFile(
path.join(pathToDrive, 'rename', 'file.txt'),
'some file content'
)
await createFile(
path.join(pathToDrive, 'rename', 'exists.txt'),
'some existing content'
)
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' })
.expect(409)
expect(res.text).toEqual('File with new name already exists.')
})
})
})
const getExampleService = (): ServiceMember =>
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
.members[0] as ServiceMember
const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({
clientId,
userId
})
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
return adminAccessToken
}

View File

@@ -4,7 +4,13 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
import User from '../../../model/User'
const clientId = 'someclientID'
const adminUser = {
@@ -27,6 +33,12 @@ const group = {
description: 'DC group for testing purposes.'
}
const PUBLIC_GROUP = {
name: PUBLIC_GROUP_NAME,
description:
'A special group that can be used to bypass authentication for particular routes.'
}
const userController = new UserController()
const groupController = new GroupController()
@@ -535,6 +547,64 @@ describe('group', () => {
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request when adding user to Public group', async () => {
const dbGroup = await groupController.createGroup(PUBLIC_GROUP)
const dbUser = await userController.createUser({
...user,
username: 'publicUser'
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(400)
expect(res.text).toEqual(
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
)
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'ldapGroupUser'
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'ldapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
})
describe('RemoveUser', () => {
@@ -586,6 +656,46 @@ describe('group', () => {
expect(res.body.groups).toEqual([])
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'removeLdapGroupUser'
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'removeLdapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')

View File

@@ -0,0 +1,596 @@
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import {
DriveController,
UserController,
GroupController,
PermissionController,
PrincipalType,
PermissionType,
PermissionSettingForRoute
} from '../../../controllers/'
import {
UserDetailsResponse,
PermissionDetailsResponse
} from '../../../controllers'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
const deployPayload = {
appLoc: 'string',
streamWebFolder: 'string',
fileTree: {
members: [
{
name: 'string',
type: 'folder',
members: [
'string',
{
name: 'string',
type: 'service',
code: 'string'
}
]
}
]
}
}
const clientId = 'someclientID'
const adminUser = {
displayName: 'Test Admin',
username: 'testAdminUsername',
password: '12345678',
isAdmin: true,
isActive: true
}
const user = {
displayName: 'Test User',
username: 'testUsername',
password: '87654321',
isAdmin: false,
isActive: true
}
const permission = {
path: '/SASjsApi/code/execute',
type: PermissionType.route,
setting: PermissionSettingForRoute.grant,
principalType: PrincipalType.user
}
const group = {
name: 'DCGroup1',
description: 'DC group for testing purposes.'
}
const userController = new UserController()
const groupController = new GroupController()
const permissionController = new PermissionController()
describe('permission', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
let dbUser: UserDetailsResponse
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
adminAccessToken = await generateSaveTokenAndCreateUser()
dbUser = await userController.createUser(user)
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('create', () => {
afterEach(async () => {
await deleteAllPermissions()
})
it('should respond with new permission when principalType is user', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.id })
.expect(200)
expect(res.body.permissionId).toBeTruthy()
expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting)
expect(res.body.user).toBeTruthy()
})
it('should respond with new permission when principalType is group', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalType: 'group',
principalId: dbGroup.groupId
})
.expect(200)
expect(res.body.permissionId).toBeTruthy()
expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting)
expect(res.body.group).toBeTruthy()
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.send(permission)
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Unauthorized if access token is not of an admin account', async () => {
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.post('/SASjsApi/permission')
.auth(accessToken, { type: 'bearer' })
.send(permission)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if path is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
path: undefined
})
.expect(400)
expect(res.text).toEqual(`"path" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if path is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
path: '/some/random/api/endpoint'
})
.expect(400)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if type is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
type: 'invalid'
})
.expect(400)
expect(res.text).toEqual('"type" must be [Route]')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if type is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
type: undefined
})
.expect(400)
expect(res.text).toEqual(`"type" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
setting: undefined
})
.expect(400)
expect(res.text).toEqual(`"setting" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
setting: 'invalid'
})
.expect(400)
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principalType is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalType: undefined
})
.expect(400)
expect(res.text).toEqual(`"principalType" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principal type is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalType: 'invalid'
})
.expect(400)
expect(res.text).toEqual('"principalType" must be one of [user, group]')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principalId is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalId: undefined
})
.expect(400)
expect(res.text).toEqual(`"principalId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principalId is not a number', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalId: 'someCharacters'
})
.expect(400)
expect(res.text).toEqual('"principalId" must be a number')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if adding permission for admin user', async () => {
const adminUser = await userController.createUser({
...user,
username: 'adminUser',
isAdmin: true
})
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalId: adminUser.id
})
.expect(400)
expect(res.text).toEqual('Can not add permission for admin user.')
expect(res.body).toEqual({})
})
it('should respond with Not Found (404) if user is not found', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalId: 123
})
.expect(404)
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
it('should respond with Not Found (404) if group is not found', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalType: 'group',
principalId: 123
})
.expect(404)
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
it('should respond with Conflict (409) if permission already exists', async () => {
await permissionController.createPermission({
...permission,
principalId: dbUser.id
})
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.id })
.expect(409)
expect(res.text).toEqual(
'Permission already exists with provided Path, Type and User.'
)
expect(res.body).toEqual({})
})
})
describe('update', () => {
let dbPermission: PermissionDetailsResponse | undefined
beforeAll(async () => {
dbPermission = await permissionController.createPermission({
...permission,
principalId: dbUser.id
})
})
afterEach(async () => {
await deleteAllPermissions()
})
it('should respond with updated permission', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ setting: PermissionSettingForRoute.deny })
.expect(200)
expect(res.body.setting).toEqual('Deny')
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Unauthorized if access token is not of an admin account', async () => {
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'update' + user.username
})
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is missing', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(400)
expect(res.text).toEqual(`"setting" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is invalid', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({
setting: 'invalid'
})
.expect(400)
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
expect(res.body).toEqual({})
})
it('should respond with not found (404) if permission with provided id does not exist', async () => {
const res = await request(app)
.patch('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' })
.send({
setting: PermissionSettingForRoute.deny
})
.expect(404)
expect(res.text).toEqual('Permission not found.')
expect(res.body).toEqual({})
})
})
describe('delete', () => {
it('should delete permission', async () => {
const dbPermission = await permissionController.createPermission({
...permission,
principalId: dbUser.id
})
const res = await request(app)
.delete(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.text).toEqual('Permission Deleted!')
})
it('should respond with not found (404) if permission with provided id does not exists', async () => {
const res = await request(app)
.delete('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Permission not found.')
})
})
describe('get', () => {
beforeAll(async () => {
await permissionController.createPermission({
...permission,
path: '/test-1',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/test-2',
principalId: dbUser.id
})
})
it('should give a list of all permissions when user is admin', async () => {
const res = await request(app)
.get('/SASjsApi/permission/')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toHaveLength(2)
})
it(`should give a list of user's own permissions when user is not admin`, async () => {
const nonAdminUser = await userController.createUser({
...user,
username: 'get' + user.username
})
const accessToken = await generateAndSaveToken(nonAdminUser.id)
await permissionController.createPermission({
path: '/test-1',
type: PermissionType.route,
principalType: PrincipalType.user,
principalId: nonAdminUser.id,
setting: PermissionSettingForRoute.grant
})
const permissionCount = 1
const res = await request(app)
.get('/SASjsApi/permission/')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toHaveLength(permissionCount)
})
})
describe('verify', () => {
beforeAll(async () => {
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/deploy',
principalId: dbUser.id
})
})
beforeEach(() => {
jest
.spyOn(DriveController.prototype, 'deploy')
.mockImplementation((deployPayload) =>
Promise.resolve({
status: 'success',
message: 'Files deployed successfully to @sasjs/server.'
})
)
})
afterEach(() => {
jest.resetAllMocks()
})
it('should create files in SASJS drive', async () => {
const accessToken = await generateAndSaveToken(dbUser.id)
await request(app)
.get('/SASjsApi/drive/deploy')
.auth(accessToken, { type: 'bearer' })
.send(deployPayload)
.expect(200)
})
it('should respond unauthorized', async () => {
const accessToken = await generateAndSaveToken(dbUser.id)
await request(app)
.get('/SASjsApi/drive/deploy/upload')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
})
})
})
const generateSaveTokenAndCreateUser = async (
someUser?: any
): Promise<string> => {
const dbUser = await userController.createUser(someUser ?? adminUser)
return generateAndSaveToken(dbUser.id)
}
const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({
clientId,
userId
})
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
return adminAccessToken
}
const deleteAllPermissions = async () => {
const { collections } = mongoose.connection
const collection = collections['permissions']
await collection.deleteMany({})
}

View File

@@ -4,7 +4,13 @@ import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import {
UserController,
PermissionController,
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
@@ -15,11 +21,11 @@ import {
} from '../../../utils'
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController
SessionController,
SASSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types'
import { Session, SessionState } from '../../../types'
const clientId = 'someclientID'
@@ -33,20 +39,33 @@ const user = {
const sampleSasProgram = '%put hello world!;'
const sampleJsProgram = `console.log('hello world!/')`
const samplePyProgram = `print('hello world!/')`
const filesFolder = getFilesFolder()
const testFilesFolder = `test-stp-${generateTimestamp()}`
let app: Express
let accessToken: string
describe('stp', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let accessToken: string
const userController = new UserController()
const permissionController = new PermissionController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
accessToken = await generateSaveTokenAndCreateUser(user)
const dbUser = await userController.createUser(user)
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
path: '/SASjsApi/stp/execute',
type: PermissionType.route,
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSettingForRoute.grant
})
})
afterAll(async () => {
@@ -56,8 +75,6 @@ describe('stp', () => {
})
describe('execute', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
describe('get', () => {
describe('with runtime js', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
@@ -77,41 +94,45 @@ describe('stp', () => {
})
it('should execute js program when both js and sas program are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.JS, RunTimeType.SAS],
200,
RunTimeType.JS
)
})
it('should throw error when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await makeRequestAndAssert([], 400)
})
})
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime py', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
beforeAll(() => {
process.runTimes = [RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute python program when python, js and sas programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.PY
)
})
it('should throw error when py program is not present but js or sas program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
@@ -131,41 +152,11 @@ describe('stp', () => {
})
it('should execute sas program when both sas and js programs are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
it('should throw error when sas program do not exit but js exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
await makeRequestAndAssert([], 400)
})
})
@@ -185,63 +176,51 @@ describe('stp', () => {
})
it('should execute js program when both js and sas program are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.JS
)
})
it('should execute sas program when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await makeRequestAndAssert([], 400)
})
})
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime py and sas', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.PY, RunTimeType.SAS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute python program when both python and sas program are present', async () => {
await makeRequestAndAssert(
[RunTimeType.PY, RunTimeType.SAS],
200,
RunTimeType.PY
)
})
it('should execute sas program when python program is not present but sas program exists', async () => {
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
it('should throw error when both sas and js programs do not exist', async () => {
await makeRequestAndAssert([], 400)
})
})
@@ -261,76 +240,220 @@ describe('stp', () => {
})
it('should execute sas program when both sas and js programs exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.SAS
)
})
it('should execute js program when sas program is not present but js program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await makeRequestAndAssert([], 400)
})
})
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime sas and py', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.SAS, RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute sas program when both sas and python programs exist', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.PY],
200,
RunTimeType.SAS
)
})
it('should execute python program when sas program is not present but python program exists', async () => {
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
})
it('should throw error when both sas and python programs do not exist', async () => {
await makeRequestAndAssert([], 400)
})
})
describe('with runtime sas, js and py', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.SAS, RunTimeType.JS, RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute sas program when it exists, no matter js and python programs exist or not', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.PY, RunTimeType.JS],
200,
RunTimeType.SAS
)
})
it('should execute js program when sas program is absent but js and python programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.JS, RunTimeType.PY],
200,
RunTimeType.JS
)
})
it('should execute python program when both sas and js programs are not present', async () => {
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
})
it('should throw error when no program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
describe('with runtime js, sas and py', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute js program when it exists, no matter sas and python programs exist or not', async () => {
await makeRequestAndAssert(
[RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY],
200,
RunTimeType.JS
)
})
it('should execute sas program when js program is absent but sas and python programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.PY],
200,
RunTimeType.SAS
)
})
it('should execute python program when both sas and js programs are not present', async () => {
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
})
it('should throw error when no program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
describe('with runtime py, sas and js', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute python program when it exists, no matter sas and js programs exist or not', async () => {
await makeRequestAndAssert(
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.PY
)
})
it('should execute sas program when python program is absent but sas and js programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.SAS
)
})
it('should execute js program when both sas and python programs are not present', async () => {
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
})
it('should throw error when no program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
})
})
})
const generateSaveTokenAndCreateUser = async (
someUser: any
): Promise<string> => {
const userController = new UserController()
const dbUser = await userController.createUser(someUser)
const makeRequestAndAssert = async (
programTypes: RunTimeType[],
expectedStatusCode: number,
expectedRuntime?: RunTimeType
) => {
const programPath = path.join(testFilesFolder, 'program')
for (const programType of programTypes) {
if (programType === RunTimeType.JS)
await createFile(
path.join(filesFolder, `${programPath}.js`),
sampleJsProgram
)
else if (programType === RunTimeType.PY)
await createFile(
path.join(filesFolder, `${programPath}.py`),
samplePyProgram
)
else if (programType === RunTimeType.SAS)
await createFile(
path.join(filesFolder, `${programPath}.sas`),
sampleSasProgram
)
}
return generateAndSaveToken(dbUser.id)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(expectedStatusCode)
if (expectedRuntime)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expectedRuntime,
expect.anything(),
undefined
)
}
const generateAndSaveToken = async (userId: number) => {
@@ -348,7 +471,7 @@ const setupMocks = async () => {
.mockImplementation(mockedGetSession)
jest
.spyOn(JSSessionController.prototype, 'getSession')
.spyOn(SASSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
@@ -370,10 +493,7 @@ const mockedGetSession = async () => {
const session: Session = {
id: sessionId,
ready: true,
inUse: true,
consumed: false,
completed: false,
state: SessionState.pending,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder

View File

@@ -4,7 +4,12 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import User from '../../../model/User'
const clientId = 'someclientID'
const adminUser = {
@@ -110,16 +115,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
await controller.createUser(user)
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send(user)
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
@@ -226,6 +231,36 @@ describe('user', () => {
.expect(400)
})
it('should respond with Method Not Allowed, when updating username of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newUsername = 'newUsername'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ username: newUsername })
.expect(405)
})
it('should respond with Method Not Allowed, when updating displayName of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newDisplayName = 'My new display Name'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ displayName: newDisplayName })
.expect(405)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/user/1234')
@@ -254,7 +289,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -265,9 +300,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
@@ -349,7 +384,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -360,9 +395,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
})
@@ -446,7 +481,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
@@ -454,9 +489,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
@@ -528,7 +563,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
@@ -536,9 +571,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
})
@@ -652,16 +687,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if userId is incorrect', async () => {
it('should respond with Not Found if userId is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User is not found.')
expect(res.text).toEqual('User is not found.')
expect(res.body).toEqual({})
})
@@ -731,16 +766,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is incorrect', async () => {
it('should respond with Not Found if username is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/by/username/randomUsername')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User is not found.')
expect(res.text).toEqual('User is not found.')
expect(res.body).toEqual({})
})
})
@@ -770,12 +805,14 @@ describe('user', () => {
{
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
id: expect.anything(),
username: user.username,
displayName: user.displayName
displayName: user.displayName,
isAdmin: user.isAdmin
}
])
})
@@ -796,12 +833,14 @@ describe('user', () => {
{
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
id: expect.anything(),
username: 'randomUser',
displayName: user.displayName
displayName: user.displayName,
isAdmin: user.isAdmin
}
])
})

View File

@@ -39,21 +39,90 @@ describe('web', () => {
describe('home', () => {
it('should respond with CSRF Token', async () => {
await request(app)
.get('/')
.expect(
'set-cookie',
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
)
const res = await request(app).get('/').expect(200)
expect(res.text).toMatch(
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
)
})
})
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
let cookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
;({ csrfToken } = await getCSRF(app))
})
afterEach(async () => {
@@ -67,7 +136,6 @@ describe('web', () => {
const res = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
@@ -79,73 +147,114 @@ describe('web', () => {
expect(res.body.user).toEqual({
id: expect.any(Number),
username: user.username,
displayName: user.displayName
displayName: user.displayName,
isAdmin: user.isAdmin,
needsToUpdatePassword: true
})
})
})
describe('SASLogon/authorize', () => {
let csrfToken: string
let cookies: string
let authCookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
it('should respond with too many requests when attempting with invalid password for a same user too many times', async () => {
await userController.createUser(user)
const credentials = {
username: user.username,
password: user.password
}
const promises: request.Test[] = []
;({ cookies: authCookies } = await performLogin(
app,
credentials,
cookies,
csrfToken
))
})
const maxConsecutiveFailsByUsernameAndIp = Number(
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
)
afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
Array(maxConsecutiveFailsByUsernameAndIp + 1)
.fill(0)
.map((_, i) => {
promises.push(
request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: 'invalid-password'
})
)
})
await Promise.all(promises)
it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({ clientId })
.send({
username: user.username,
password: user.password
})
.expect(429)
expect(res.body).toHaveProperty('code')
expect(res.text).toContain('Too Many Requests!')
})
it('should respond with Bad Request if clientId is missing', async () => {
it('should respond with too many requests when attempting with invalid credentials for different users but with same ip too many times', async () => {
await userController.createUser(user)
const promises: request.Test[] = []
const maxWrongAttemptsByIpPerDay = Number(
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
)
Array(maxWrongAttemptsByIpPerDay + 1)
.fill(0)
.map((_, i) => {
promises.push(
request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: `user${i}`,
password: 'invalid-password'
})
)
})
await Promise.all(promises)
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({})
.send({
username: user.username,
password: user.password
})
.expect(429)
expect(res.text).toContain('Too Many Requests!')
})
it('should respond with Bad Request if CSRF Token is not present', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.text).toEqual('Invalid CSRF token!')
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, cookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
})
.expect(403)
it('should respond with Bad Request if CSRF Token is invalid', async () => {
await userController.createUser(user)
expect(res.text).toEqual('Error: Invalid clientId.')
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
})
@@ -153,30 +262,28 @@ describe('web', () => {
const getCSRF = async (app: Express) => {
// make request to get CSRF
const { header } = await request(app).get('/')
const cookies = header['set-cookie'].join()
const { text } = await request(app).get('/')
const csrfToken = extractCSRF(cookies)
return { csrfToken, cookies }
return { csrfToken: extractCSRF(text) }
}
const performLogin = async (
app: Express,
credentials: { username: string; password: string },
cookies: string,
csrfToken: string
) => {
const { header } = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send(credentials)
const newCookies: string = header['set-cookie'].join()
return { cookies: newCookies }
return {
authCookies:
(header['set-cookie'] as unknown as string[] | undefined)?.join() || ''
}
}
const extractCSRF = (cookies: string) =>
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
cookies
)![2]
const extractCSRF = (text: string) =>
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
text
)![1]

View File

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

View File

@@ -23,7 +23,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
const response = await controller.createUser(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -33,7 +33,7 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllUsers()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -51,7 +51,7 @@ userRouter.get(
const response = await controller.getUserByUsername(req, username)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -64,7 +64,7 @@ userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const response = await controller.getUser(req, parseInt(userId))
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -91,7 +91,7 @@ userRouter.patch(
const response = await controller.updateUserByUsername(username, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -113,7 +113,7 @@ userRouter.patch(
const response = await controller.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -141,7 +141,7 @@ userRouter.delete(
await controller.deleteUserByUsername(username, data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -163,7 +163,7 @@ userRouter.delete(
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)

View File

@@ -1,5 +1,6 @@
import path from 'path'
import express, { Request } from 'express'
import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
@@ -9,10 +10,10 @@ const appStreams: { [key: string]: string } = {}
const router = express.Router()
router.get('/', async (req, res) => {
router.get('/', authenticateAccessToken, async (req, res) => {
const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken())
res.cookie('XSRF-TOKEN', generateCSRFToken())
return res.send(content)
})
@@ -57,7 +58,7 @@ export const publishAppStream = async (
)
const sasJsPort = process.env.PORT || 5000
console.log(
process.logger.info(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)
@@ -66,7 +67,7 @@ export const publishAppStream = async (
return {}
}
router.get(`/*`, function (req: Request, res, next) {
router.get(`/*`, authenticateAccessToken, function (req: Request, res, next) {
const reqPath = req.path.replace(/^\//, '')
// Redirecting to url with trailing slash for appStream base URL only

View File

@@ -26,6 +26,7 @@ export const style = `<style>
}
.app-container .app img{
width: 100%;
height: calc(100% - 30px);
margin-bottom: 10px;
border-radius: 10px;
}

View File

@@ -4,7 +4,7 @@ import webRouter from './web'
import apiRouter from './api'
import appStreamRouter from './appStream'
import { csrfProtection } from '../app'
import { csrfProtection } from '../middlewares'
export const setupRoutes = (app: Express) => {
app.use('/SASjsApi', apiRouter)
@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
appStreamRouter(req, res, next)
})
app.use('/', csrfProtection, webRouter)
app.use('/', webRouter)
}

View File

@@ -1,8 +1,26 @@
import express from 'express'
import sas9WebRouter from './sas9-web'
import sasViyaWebRouter from './sasviya-web'
import webRouter from './web'
import { MOCK_SERVERTYPEType } from '../../utils'
import { csrfProtection } from '../../middlewares'
const router = express.Router()
router.use('/', webRouter)
const { MOCK_SERVERTYPE } = process.env
switch (MOCK_SERVERTYPE) {
case MOCK_SERVERTYPEType.SAS9: {
router.use('/', sas9WebRouter)
break
}
case MOCK_SERVERTYPEType.SASVIYA: {
router.use('/', sasViyaWebRouter)
break
}
default: {
router.use('/', csrfProtection, webRouter)
}
}
export default router

View File

@@ -0,0 +1,152 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers'
import { MockSas9Controller } from '../../controllers/mock-sas9'
import multer from 'multer'
import path from 'path'
import dotenv from 'dotenv'
import { FileUploadController } from '../../controllers/internal'
dotenv.config()
const sas9WebRouter = express.Router()
const webController = new WebController()
// Mock controller must be singleton because it keeps the states
// for example `isLoggedIn` and potentially more in future mocks
const controller = new MockSas9Controller()
const fileUploadController = new FileUploadController()
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
const upload = multer({
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
})
sas9WebRouter.get('/', async (req, res) => {
let response
try {
response = await webController.home()
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(injectedContent)
}
})
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
const response = await controller.sasStoredProcess(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
const response = await controller.sasStoredProcessDoGet(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.post(
'/SASStoredProcess/do/',
fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(),
async (req, res) => {
const response = await controller.sasStoredProcessDoPost(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
const response = await controller.loginGet()
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
const response = await controller.loginPost(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASLogon/logout', async (req, res) => {
const response = await controller.logout(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASStoredProcess/Logoff', async (req, res) => {
const response = await controller.logoff(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sas9WebRouter

View File

@@ -0,0 +1,33 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
const sasViyaWebRouter = express.Router()
const controller = new WebController()
sasViyaWebRouter.get('/', async (req, res) => {
let response
try {
response = await controller.home()
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(injectedContent)
}
})
sasViyaWebRouter.post('/SASJobExecution/', async (req, res) => {
try {
res.send({ test: 'test' })
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sasViyaWebRouter

View File

@@ -1,6 +1,11 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import {
authenticateAccessToken,
bruteForceProtection,
desktopRestrict
} from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils'
const webRouter = express.Router()
@@ -11,25 +16,41 @@ webRouter.get('/', async (req, res) => {
try {
response = await controller.home()
} catch (_) {
response = 'Web Build is not present'
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
res.cookie('XSRF-TOKEN', req.csrfToken())
const { ALLOWED_DOMAIN } = process.env
const allowedDomain = ALLOWED_DOMAIN?.trim()
const domain = allowedDomain ? ` Domain=${allowedDomain};` : ''
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()};${domain} Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(response)
return res.send(injectedContent)
}
})
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
webRouter.post(
'/SASLogon/login',
desktopRestrict,
bruteForceProtection,
async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
if (err instanceof Error) {
res.status(500).send(err.toString())
} else {
res.status(err.code).send(err.message)
}
}
}
})
)
webRouter.post(
'/SASLogon/authorize',
@@ -48,7 +69,7 @@ webRouter.post(
}
)
webRouter.get('/logout', desktopRestrict, async (req, res) => {
webRouter.get('/SASLogon/logout', desktopRestrict, async (req, res) => {
try {
await controller.logout(req)
res.status(200).send('OK!')

View File

@@ -7,20 +7,20 @@ appPromise.then(async (app) => {
const protocol = process.env.PROTOCOL || 'http'
const sasJsPort = process.env.PORT || 5000
console.log('PROTOCOL: ', protocol)
process.logger.info('PROTOCOL: ', protocol)
if (protocol !== 'https') {
app.listen(sasJsPort, () => {
console.log(
process.logger.info(
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
)
})
} else {
const { key, cert } = await getCertificates()
const { key, cert, ca } = await getCertificates()
const httpsServer = createServer({ key, cert }, app)
const httpsServer = createServer({ key, cert, ca }, app)
httpsServer.listen(sasJsPort, () => {
console.log(
process.logger.info(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
)
})

View File

@@ -5,5 +5,6 @@ export interface RequestUser {
displayName: string
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec?: string
}

View File

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

View File

@@ -2,5 +2,6 @@ export interface TreeNode {
name: string
relativePath: string
absolutePath: string
isFolder: boolean
children: Array<TreeNode>
}

View File

@@ -1,12 +1,18 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
nodeLoc: string
sasLoc?: string
nodeLoc?: string
pythonLoc?: string
rLoc?: string
driveLoc: string
sasjsRoot: string
logsLoc: string
logsUUID: string
sessionController?: import('../../controllers/internal').SessionController
sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]
secrets: import('../../model/Configuration').ConfigurationType
}
}

View File

@@ -5,6 +5,8 @@ import { AppStreamConfig } from '../types'
import { getAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
process.appStreamConfig = {}
if (process.env.NODE_ENV === 'test') return
const appStreamConfigPath = getAppStreamConfigPath()
@@ -21,7 +23,6 @@ export const loadAppStreamConfig = async () => {
} catch (_) {
appStreamConfig = {}
}
process.appStreamConfig = {}
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
const { appLoc, streamWebFolder, streamLogo } = entry
@@ -35,7 +36,7 @@ export const loadAppStreamConfig = async () => {
)
}
console.log('App Stream Config loaded!')
process.logger.info('App Stream Config loaded!')
}
export const addEntryToAppStreamConfig = (

View File

@@ -8,8 +8,6 @@ export const connectDB = async () => {
throw new Error('Unable to connect to DB!')
}
console.log('Connected to DB!')
await seedDB()
return mongoose.connection
process.logger.success('Connected to DB!')
return seedDB()
}

View File

@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
if (process.env.NODE_ENV === 'test') return
console.log('Copying Macros from container to drive(tmp).')
process.logger.log('Copying Macros from container to drive.')
const macrosDrivePath = getMacrosFolder()
@@ -30,5 +30,5 @@ export const copySASjsCore = async () => {
await createFile(macroFileDestPath, macroContent)
})
console.log('Macros Drive Path:', macrosDrivePath)
process.logger.info('Macros Drive Path:', macrosDrivePath)
}

View File

@@ -0,0 +1,18 @@
import path from 'path'
import { createFile } from '@sasjs/utils'
import { getMacrosFolder } from './file'
const fileContent = `%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO,maxobs=MAX);
%ms_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt
,missing=&missing
,showmeta=&showmeta
,maxobs=&maxobs
)
%mend;`
export const createWeboutSasFile = async () => {
const macrosDrivePath = getMacrosFolder()
process.logger.log(`Creating webout.sas at ${macrosDrivePath}`)
const filePath = path.join(macrosDrivePath, 'webout.sas')
await createFile(filePath, fileContent)
}

View File

@@ -10,7 +10,7 @@ export const sysInitCompiledPath = path.join(
'systemInitCompiled.sas'
)
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const sasJSCoreMacros = path.join(apiRoot, 'sas', 'sasautos')
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
@@ -20,19 +20,24 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
export const getDesktopUserAutoExecPath = () =>
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
export const getSasjsRootFolder = () => process.driveLoc
export const getSasjsRootFolder = () => process.sasjsRoot
export const getSasjsDriveFolder = () => process.driveLoc
export const getLogFolder = () => process.logsLoc
export const getAppStreamConfigPath = () =>
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
export const getMacrosFolder = () =>
path.join(getSasjsRootFolder(), 'sasjscore')
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
export const getPackagesFolder = () =>
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')

View File

@@ -1,7 +1,8 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateAccessToken = (data: InfoJWT) =>
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
expiresIn: '1day'
export const generateAccessToken = (data: InfoJWT, expiry?: number) =>
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
})

View File

@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
export const generateAuthCode = (data: InfoJWT) =>
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
jwt.sign(data, process.secrets.AUTH_CODE_SECRET, {
expiresIn: '30s'
})

View File

@@ -1,7 +1,8 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateRefreshToken = (data: InfoJWT) =>
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
expiresIn: '30 days'
export const generateRefreshToken = (data: InfoJWT, expiry?: number) =>
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
})

Some files were not shown because too many files have changed in this diff Show More