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

Compare commits

...

488 Commits

Author SHA1 Message Date
semantic-release-bot
1e2c08a8d3 chore(release): 0.7.0 [skip ci]
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)

### Bug Fixes

* add runtimes to global process object ([194eaec](194eaec7d4))
* code fixes for executing program from program path including file extension ([53854d0](53854d0012))
* code/execute controller logic to handle different runtimes ([23b6692](23b6692f02))
* convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram ([c58666e](c58666eb81))
* no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code ([9d5a5e0](9d5a5e051f))
* pass _program to execute file without extension ([5df619b](5df619b3f6))
* refactor code for session selection in preUploadMiddleware function ([b444381](b4443819d4))
* refactor code in executeFile method of session controller ([dffe6d7](dffe6d7121))
* refactor code in preUploadMiddleware function ([6d6bda5](6d6bda5626))
* refactor sas/js session controller classes to inherit from base session controller class ([2c704a5](2c704a544f))
* **Studio:** style fix for runtime dropdown ([9023cf3](9023cf33b5))

### Features

* configure child process with writeStream to write logs to log file ([058b3b0](058b3b0081))
* conver single session controller to two controller i.e. SASSessionController and JSSessionController ([07295aa](07295aa151))
* create and inject code for uploaded files to code.js ([1685616](16856165fb))
* validate sasjs_runtimes env var ([596ada7](596ada7ca8))
2022-06-19 13:44:02 +00:00
Saad Jutt
473fbd62c0 Merge pull request #186 from sasjs/issue-184
feat: Support JS runtime for services
2022-06-19 06:39:11 -07:00
Saad Jutt
b1a0fe7060 chore: code fixes 2022-06-19 18:35:10 +05:00
dde293c852 chore: write specs for get stp/execute 2022-06-19 07:08:36 +05:00
f738a6d7a3 chore: splitted functions into different files 2022-06-19 07:07:39 +05:00
Saad Jutt
3e0a2de2ad chore: nvmrc version updated to LTS 2022-06-18 01:49:47 +05:00
Saad Jutt
91cb7bd946 chore(workflow): CI/CD env variables updated 2022-06-18 01:13:56 +05:00
Saad Jutt
a501a300dc chore: lint fixes 2022-06-18 00:53:05 +05:00
Saad Jutt
b446baa822 chore: Merge branch 'main' into issue-184 2022-06-18 00:51:48 +05:00
9023cf33b5 fix(Studio): style fix for runtime dropdown 2022-06-17 23:17:23 +05:00
23b6692f02 fix: code/execute controller logic to handle different runtimes 2022-06-17 20:01:50 +05:00
Allan Bowe
6de91618ff Merge pull request #201 from sasjs/all-contributors/add-kknapen
docs: add kknapen as a contributor for userTesting
2022-06-17 16:33:41 +02:00
allcontributors[bot]
e06d66f312 docs: update .all-contributorsrc [skip ci] 2022-06-17 14:33:12 +00:00
allcontributors[bot]
1ffaf2e0ef docs: update README.md [skip ci] 2022-06-17 14:33:11 +00:00
Allan Bowe
393d3327db docs: add @VladislavParhomchik as a contributor 2022-06-17 14:32:52 +00:00
Allan Bowe
14cfb9a663 docs: add @allanbowe as a contributor 2022-06-17 14:29:14 +00:00
Allan Bowe
dd1f2b3ed7 docs: add @medjedovicm as a contributor 2022-06-17 14:29:06 +00:00
Allan Bowe
9f5dbbc8da docs: add @yuryshkoda as a contributor 2022-06-17 14:28:48 +00:00
Allan Bowe
9423bb2b23 docs: add @sabhas as a contributor 2022-06-17 14:28:39 +00:00
Allan Bowe
5bfcdc4dbb docs: add @saadjutt01 as a contributor 2022-06-17 14:27:53 +00:00
semantic-release-bot
ecd8ed9032 chore(release): 0.6.1 [skip ci]
## [0.6.1](https://github.com/sasjs/server/compare/v0.6.0...v0.6.1) (2022-06-17)

### Bug Fixes

* home page wording.  Using fix to force previous change through.. ([8702a4e](8702a4e8fd))
2022-06-17 13:19:51 +00:00
Allan Bowe
a8d89ff1d6 Merge pull request #200 from sasjs/nit
fix: home page wording.  Using fix to force previous change through..
2022-06-17 15:14:59 +02:00
Allan Bowe
8702a4e8fd fix: home page wording. Using fix to force previous change through.. 2022-06-17 13:14:02 +00:00
ab222cbaab chore: verify executable paths 2022-06-17 18:12:03 +05:00
Allan Bowe
5f06132ece Merge pull request #199 from sasjs/allanbowe-patch-1
Update Session.ts
2022-06-17 14:55:22 +02:00
Allan Bowe
56c80b0979 Update Session.ts 2022-06-17 13:46:08 +01:00
158acf1f97 chore: set sas,js as default run times 2022-06-17 04:02:40 +05:00
semantic-release-bot
c19a20c1d4 chore(release): 0.6.0 [skip ci]
# [0.6.0](https://github.com/sasjs/server/compare/v0.5.0...v0.6.0) (2022-06-16)

### Features

* get group by group name ([6b0b94a](6b0b94ad38))
2022-06-16 18:43:40 +00:00
Allan Bowe
f8eaadae7b Merge pull request #197 from sasjs/fetch-group-by-name
feat: get group by group name
2022-06-16 20:39:23 +02:00
90e0973a7f chore: add test for group name validation 2022-06-16 23:36:13 +05:00
869a13fc69 chore: error code fixes 2022-06-16 23:27:56 +05:00
1790e10fc1 chore: code fixes 2022-06-16 22:14:47 +05:00
semantic-release-bot
6d12b900ad chore(release): 0.5.0 [skip ci]
# [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16)

### Bug Fixes

* npm audit fix to avoid warnings on npm i ([28a6a36](28a6a36bb7))

### Features

* **api:** deployment through zipped/compressed file ([b81d742](b81d742c6c))
2022-06-16 13:31:09 +00:00
Saad Jutt
ae5aa02733 Merge pull request #196 from sasjs/issue173
feat(api): deployment through zipped/compressed file
2022-06-16 06:26:32 -07:00
Allan Bowe
28a6a36bb7 fix: npm audit fix to avoid warnings on npm i 2022-06-16 13:07:59 +00:00
Saad Jutt
4e7579dc10 chore(specs): specs added for deploy upload file and zipped file 2022-06-16 17:58:56 +05:00
6b0b94ad38 feat: get group by group name 2022-06-16 13:06:33 +05:00
Saad Jutt
b81d742c6c feat(api): deployment through zipped/compressed file 2022-06-16 00:56:51 +05:00
semantic-release-bot
a61adbcac2 chore(release): 0.4.2 [skip ci]
## [0.4.2](https://github.com/sasjs/server/compare/v0.4.1...v0.4.2) (2022-06-15)

### Bug Fixes

* appStream redesign ([73792fb](73792fb574))
2022-06-15 15:04:11 +00:00
Allan Bowe
12000f4fc7 Merge pull request #195 from sasjs/appStream-design
fix: appStream redesign
2022-06-15 16:59:58 +02:00
73792fb574 fix: appStream redesign 2022-06-15 15:51:42 +02:00
53854d0012 fix: code fixes for executing program from program path including file extension 2022-06-15 16:18:07 +05:00
Saad Jutt
81501d17ab chore: code fixes 2022-06-15 16:03:04 +05:00
Saad Jutt
11a7f920f1 chore: Merge branch 'main' into issue-184 2022-06-15 15:56:17 +05:00
semantic-release-bot
c08cfcbc38 chore(release): 0.4.1 [skip ci]
## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15)

### Bug Fixes

* add/remove group to User when adding/removing user from group and return group membership on getting user ([e08bbcc](e08bbcc543))
2022-06-15 10:38:22 +00:00
Saad Jutt
8d38d5ac64 Merge pull request #193 from sasjs/issue-192
fix: add/remove group to User when adding/removing user from group
2022-06-15 03:32:32 -07:00
e08bbcc543 fix: add/remove group to User when adding/removing user from group and return group membership on getting user 2022-06-15 15:18:42 +05:00
semantic-release-bot
eef3cb270d chore(release): 0.4.0 [skip ci]
# [0.4.0](https://github.com/sasjs/server/compare/v0.3.10...v0.4.0) (2022-06-14)

### Features

* new APIs added for GET|PATCH|DELETE of user by username ([aef411a](aef411a0ea))
2022-06-14 17:28:50 +00:00
Saad Jutt
9cfbca23f8 Merge pull request #194 from sasjs/issue188
feat: new APIs added for GET|PATCH|DELETE of user by username
2022-06-14 10:24:42 -07:00
Saad Jutt
aef411a0ea feat: new APIs added for GET|PATCH|DELETE of user by username 2022-06-14 22:08:56 +05:00
Saad Jutt
e359265c4b chore: quick fix 2022-06-14 17:05:40 +05:00
Saad Jutt
8e7c9e671c chore: quick fix 2022-06-14 17:05:13 +05:00
Saad Jutt
c830f44e29 chore: code fixes 2022-06-14 16:48:58 +05:00
semantic-release-bot
806ea4cb5c chore(release): 0.3.10 [skip ci]
## [0.3.10](https://github.com/sasjs/server/compare/v0.3.9...v0.3.10) (2022-06-14)

### Bug Fixes

* correct syntax for encoding option ([32d372b](32d372b42f))
2022-06-14 09:53:53 +00:00
Allan Bowe
7205072358 Merge pull request #191 from sasjs/encodingfix
fix: correct syntax for encoding option
2022-06-14 11:49:38 +02:00
Allan Bowe
32d372b42f fix: correct syntax for encoding option 2022-06-14 09:49:05 +00:00
semantic-release-bot
e059bee7dc chore(release): 0.3.9 [skip ci]
## [0.3.9](https://github.com/sasjs/server/compare/v0.3.8...v0.3.9) (2022-06-14)

### Bug Fixes

* forcing utf 8 encoding. Closes [#76](https://github.com/sasjs/server/issues/76) ([8734489](8734489cf0))
2022-06-14 09:20:37 +00:00
Allan Bowe
6f56aafab1 Merge pull request #190 from sasjs/allanbowe/enforce-utf-76
fix: forcing utf 8 encoding. Closes #76
2022-06-14 11:14:35 +02:00
Allan Bowe
8734489cf0 fix: forcing utf 8 encoding. Closes #76 2022-06-14 09:12:41 +00:00
de9ed15286 chore: update error message when stored program not found 2022-06-13 20:51:44 +05:00
325285f447 Merge branch 'main' into issue-184 2022-06-13 20:42:28 +05:00
semantic-release-bot
7e6635f40f chore(release): 0.3.8 [skip ci]
## [0.3.8](https://github.com/sasjs/server/compare/v0.3.7...v0.3.8) (2022-06-13)

### Bug Fixes

* execution controller better error handling ([8a617a7](8a617a73ae))
* execution controller error details ([3fa2a7e](3fa2a7e2e3))
2022-06-13 12:32:32 +00:00
Allan Bowe
c0022a22f4 Merge pull request #189 from sasjs/issue-187
Execution controller more details in error message
2022-06-13 14:27:12 +02:00
Mihajlo Medjedovic
3fa2a7e2e3 fix: execution controller error details 2022-06-13 12:25:06 +00:00
8a617a73ae fix: execution controller better error handling 2022-06-13 14:01:12 +02:00
16856165fb feat: create and inject code for uploaded files to code.js 2022-06-09 14:54:11 +05:00
semantic-release-bot
e7babb9f55 chore(release): 0.3.7 [skip ci]
## [0.3.7](https://github.com/sasjs/server/compare/v0.3.6...v0.3.7) (2022-06-08)

### Bug Fixes

* **appstream:** redirect to relative + nested resource should be accessed ([5ab35b0](5ab35b02c4))
2022-06-08 20:21:22 +00:00
Saad Jutt
5ab35b02c4 fix(appstream): redirect to relative + nested resource should be accessed 2022-06-09 01:16:25 +05:00
058b3b0081 feat: configure child process with writeStream to write logs to log file 2022-06-08 02:01:31 +05:00
9d5a5e051f fix: no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code 2022-06-07 13:27:18 +05:00
2c704a544f fix: refactor sas/js session controller classes to inherit from base session controller class 2022-06-06 17:24:19 +05:00
6d6bda5626 fix: refactor code in preUploadMiddleware function 2022-06-06 17:23:09 +05:00
dffe6d7121 fix: refactor code in executeFile method of session controller 2022-06-06 15:23:42 +05:00
b4443819d4 fix: refactor code for session selection in preUploadMiddleware function 2022-06-06 15:19:39 +05:00
e5a7674fa1 chore: refactor ExecutionController class to remove code duplications 2022-06-06 09:09:21 +05:00
596ada7ca8 feat: validate sasjs_runtimes env var 2022-06-04 03:16:07 +05:00
f561ba4bf0 chore: documented sasjs_runtimes env variable in readme file 2022-06-04 03:12:51 +05:00
c58666eb81 fix: convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram 2022-06-03 17:26:21 +05:00
5df619b3f6 fix: pass _program to execute file without extension 2022-06-03 17:24:29 +05:00
07295aa151 feat: conver single session controller to two controller i.e. SASSessionController and JSSessionController 2022-06-03 17:23:28 +05:00
194eaec7d4 fix: add runtimes to global process object 2022-06-03 17:19:12 +05:00
semantic-release-bot
ad82ee7106 chore(release): 0.3.6 [skip ci]
## [0.3.6](https://github.com/sasjs/server/compare/v0.3.5...v0.3.6) (2022-06-02)

### Bug Fixes

* **appstream:** should serve only new files for same app stream name with new deployment ([e6d1989](e6d1989847))
2022-06-02 08:30:39 +00:00
Allan Bowe
d2e9456d81 Merge pull request #185 from sasjs/issue183
fix(appstream): should serve only new files for same app stream name …
2022-06-02 11:25:27 +03:00
Saad Jutt
e6d1989847 fix(appstream): should serve only new files for same app stream name with new deployment 2022-06-02 04:17:12 +05:00
semantic-release-bot
7a932383b4 chore(release): 0.3.5 [skip ci]
## [0.3.5](https://github.com/sasjs/server/compare/v0.3.4...v0.3.5) (2022-05-30)

### Bug Fixes

* bumping sasjs/core library ([61815f8](61815f8ae1))
2022-05-30 18:07:50 +00:00
Allan Bowe
576e18347e Merge pull request #182 from sasjs/bumpcore
fix: bumping sasjs/core library
2022-05-30 21:02:59 +03:00
Allan Bowe
61815f8ae1 fix: bumping sasjs/core library 2022-05-30 18:02:30 +00:00
semantic-release-bot
afff27fd21 chore(release): 0.3.4 [skip ci]
## [0.3.4](https://github.com/sasjs/server/compare/v0.3.3...v0.3.4) (2022-05-30)

### Bug Fixes

* **web:** system username for DESKTOP mode ([a8ba378](a8ba378fd1))
2022-05-30 16:12:22 +00:00
Saad Jutt
a8ba378fd1 fix(web): system username for DESKTOP mode 2022-05-30 21:08:17 +05:00
semantic-release-bot
73c81a45dc chore(release): 0.3.3 [skip ci]
## [0.3.3](https://github.com/sasjs/server/compare/v0.3.2...v0.3.3) (2022-05-30)

### Bug Fixes

* usage of autoexec API in DESKTOP mode ([12d424a](12d424acce))
2022-05-30 12:18:45 +00:00
Saad Jutt
12d424acce fix: usage of autoexec API in DESKTOP mode 2022-05-30 17:12:17 +05:00
Saad Jutt
414fb19de3 chore: code changes 2022-05-30 00:32:05 +05:00
semantic-release-bot
cfddf1fb0c chore(release): 0.3.2 [skip ci]
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)

### Bug Fixes

* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](2c259fe1de))
2022-05-27 19:43:00 +00:00
Muhammad Saad
1f483b1afc Merge pull request #180 from sasjs/desktop-autoexec
fix(web): ability to use get/patch User API in desktop mode.
2022-05-27 12:39:17 -07:00
Saad Jutt
0470239ef1 chore: quick fix 2022-05-27 17:35:58 +05:00
Saad Jutt
2c259fe1de fix(web): ability to use get/patch User API in desktop mode. 2022-05-27 17:01:14 +05:00
semantic-release-bot
b066734398 chore(release): 0.3.1 [skip ci]
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)

### Bug Fixes

* **api:** username should be lowercase ([5ad6ee5](5ad6ee5e0f))
* **web:** reduced width for autoexec input ([7d11cc7](7d11cc7916))
2022-05-26 15:30:45 +00:00
Allan Bowe
3b698fce5f Merge pull request #179 from sasjs/web-profile-fixes
Web profile fixes
2022-05-26 18:26:30 +03:00
Saad Jutt
5ad6ee5e0f fix(api): username should be lowercase 2022-05-26 20:20:02 +05:00
Saad Jutt
7d11cc7916 fix(web): reduced width for autoexec input 2022-05-26 19:48:59 +05:00
semantic-release-bot
ff1def6436 chore(release): 0.3.0 [skip ci]
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)

### Features

* **web:** added profile + edit + autoexec changes ([c275db1](c275db184e))
2022-05-25 23:29:22 +00:00
Saad Jutt
c275db184e feat(web): added profile + edit + autoexec changes 2022-05-26 04:25:15 +05:00
semantic-release-bot
e4239fbcc3 chore(release): 0.2.0 [skip ci]
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)

### Bug Fixes

* **autoexec:** usage in case of desktop from file ([79dc2db](79dc2dba23))

### Features

* **api:** added autoexec + major type setting changes ([2a7223a](2a7223ad7d))
2022-05-25 05:52:30 +00:00
Muhammad Saad
c6fd8fdd70 Merge pull request #178 from sasjs/issue117
feat(api): added autoexec + major type setting changes
2022-05-24 22:48:29 -07:00
Saad Jutt
79dc2dba23 fix(autoexec): usage in case of desktop from file 2022-05-25 10:44:57 +05:00
Saad Jutt
2a7223ad7d feat(api): added autoexec + major type setting changes 2022-05-24 21:12:32 +05:00
semantic-release-bot
1fed5ea6ac chore(release): 0.1.0 [skip ci]
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)

### Bug Fixes

* issue174 + issue175 + issue146 ([80b33c7](80b33c7a18))
* **web:** click to copy + notification ([f37f8e9](f37f8e95d1))

### Features

* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](53bf68a6af))
2022-05-23 21:22:02 +00:00
Muhammad Saad
97f689f292 Merge pull request #177 from sasjs/issue174
fix: issue174 + issue175 + issue146
2022-05-23 14:17:25 -07:00
Saad Jutt
53bf68a6af feat(env): added new env variable LOG_FORMAT_MORGAN 2022-05-23 21:14:37 +05:00
Saad Jutt
f37f8e95d1 fix(web): click to copy + notification 2022-05-23 20:29:29 +05:00
Saad Jutt
80b33c7a18 fix: issue174 + issue175 + issue146 2022-05-23 19:24:56 +05:00
Muhammad Saad
b1803fe385 Merge pull request #170 from sasjs/dummy-release-command
chore: added dummy release command
2022-05-16 09:42:23 -07:00
Saad Jutt
7dd08c3b5b chore: added dummy release command 2022-05-16 21:36:00 +05:00
semantic-release-bot
b780b59b66 chore(release): 0.0.77 [skip ci]
## [0.0.77](https://github.com/sasjs/server/compare/v0.0.76...v0.0.77) (2022-05-16)

### Bug Fixes

* **release:** Github workflow without npm token ([c017d13](c017d13061))
2022-05-16 16:26:07 +00:00
Muhammad Saad
7b457eaec5 Merge pull request #169 from saadjutt01/main
Release on main update
2022-05-16 09:21:54 -07:00
Saad Jutt
c017d13061 fix(release): Github workflow without npm token 2022-05-16 21:17:53 +05:00
Saad Jutt
c2b5e353a5 chore(release): 0.0.76 2022-05-16 15:30:15 +05:00
Saad Jutt
f89389bbc6 fix: get csrf token from cookie if not present in header 2022-05-16 15:30:08 +05:00
Saad Jutt
fadcc9bd29 chore(release): 0.0.75 2022-05-12 20:48:35 +05:00
Saad Jutt
182def2f3e chore(api): updated package-lock file 2022-05-12 20:48:21 +05:00
Muhammad Saad
06a5f39fea Merge pull request #166 from sasjs/deprecate-get-auth-code-api
Deprecate get auth code api
2022-05-12 08:47:40 -07:00
Saad Jutt
143b367a0e test: fixed specs 2022-05-12 20:42:50 +05:00
Saad Jutt
b5fd800300 chore: added env SESSION_SECRET to CI 2022-05-12 19:17:09 +05:00
Saad Jutt
a0b52d9982 test(web): moved authorize specs from api to web 2022-05-12 17:59:12 +05:00
Allan Bowe
c4212665c8 chore(release): 0.0.74 2022-05-12 07:53:50 +00:00
Allan Bowe
97d9bc191c Merge pull request #167 from sasjs/cspconfig
fix: csp updates
2022-05-12 10:53:21 +03:00
Allan Bowe
dd2a403985 chore: lint fix 2022-05-11 21:57:19 +00:00
Allan Bowe
7cfa2398e1 fix: csp updates 2022-05-11 21:37:49 +00:00
Saad Jutt
5888f04e08 fix(web): seperate container for auth code 2022-05-11 21:01:59 +05:00
Saad Jutt
b40de8fa6a fix: moved getAuthCode from api to web routes 2022-05-11 21:01:00 +05:00
Allan Bowe
45a2a01532 chore(release): 0.0.73 2022-05-10 11:23:59 +00:00
Allan Bowe
c61fec47c4 Merge pull request #165 from sasjs/issue-164
fix: helmet config on http mode
2022-05-10 14:01:40 +03:00
24d7f00c02 chore: type fix 2022-05-10 10:13:57 +00:00
b0fdaaaa79 fix: helmet config on http mode 2022-05-10 10:04:01 +00:00
Allan Bowe
2467616296 chore(release): 0.0.72 2022-05-09 12:33:32 +00:00
Allan Bowe
ceefbe48e9 chore(release): 0.0.71 2022-05-07 22:35:25 +00:00
Allan Bowe
426e90471e Merge pull request #163 from sasjs/issue159
fix: reqHeadrs.txt will contain headers to access APIs
2022-05-08 01:34:41 +03:00
Allan Bowe
c0b57b9e76 fix: bumping core 2022-05-07 22:31:44 +00:00
Saad Jutt
4a8e32dd20 fix: added more cookies to req 2022-05-08 03:18:04 +05:00
Saad Jutt
636301e664 fix: reqHeadrs.txt will contain headers to access APIs 2022-05-08 02:49:16 +05:00
Allan Bowe
25dc5dd215 chore(release): 0.0.70 2022-05-06 14:45:31 +00:00
Allan Bowe
503994dbd2 Merge pull request #161 from sasjs/csp-disable
Added additional options for HELMET
2022-05-06 17:44:18 +03:00
Saad Jutt
0dceb5c3c3 chore: web package-lock built with LTS 2022-05-06 19:41:02 +05:00
Mihajlo Medjedovic
1af04fa3b3 Merge branch 'csp-disable' of github.com:sasjs/server into csp-disable 2022-05-06 13:40:48 +00:00
Mihajlo Medjedovic
efa81fec77 chore: package-lock 2022-05-06 13:40:40 +00:00
Allan Bowe
10caf1918a chore: updating README 2022-05-06 12:13:45 +00:00
Mihajlo Medjedovic
4ed20a3b75 chore: readme update 2022-05-06 11:49:32 +00:00
Mihajlo Medjedovic
98b2c5fa25 chore: readme update 2022-05-06 11:46:40 +00:00
Mihajlo Medjedovic
3ad327b85f chore: helmet config cleanup 2022-05-06 11:40:12 +00:00
Mihajlo Medjedovic
dd3acce393 feat: CSP_DISABLE env option 2022-05-05 18:25:33 +00:00
Allan Bowe
8065727b9b chore(release): 0.0.69 2022-05-02 15:24:56 +00:00
Allan Bowe
e1223ec3f8 Merge pull request #158 from sasjs/update-csp-policy
fix(upload): appStream uses CSRF + Session authentication
2022-05-02 18:22:35 +03:00
Saad Jutt
1f89279264 fix(upload): appStream uses CSRF + Session authentication 2022-05-02 18:01:28 +05:00
Saad Jutt
a07f47a1ba chore(release): 0.0.68 2022-05-02 05:57:10 +05:00
Saad Jutt
2548c82dfe fix: using monaco editor locally 2022-05-02 05:57:03 +05:00
Saad Jutt
238aa1006f chore(release): 0.0.67 2022-05-02 03:41:07 +05:00
Saad Jutt
35cba97611 chore: commented helmet middleware 2022-05-02 03:40:14 +05:00
Saad Jutt
5f29dec16f chore(release): 0.0.66 2022-05-01 23:31:59 +05:00
Saad Jutt
e2a97fcb7c fix: added swagger ui init file manually 2022-05-01 23:31:48 +05:00
Allan Bowe
6adeeefcf5 chore(release): 0.0.65 2022-05-01 11:36:26 +00:00
Allan Bowe
c9d66b8576 Merge pull request #156 from sasjs/fix-swagger-api-with-csrf
fix: consume swagger api with CSRF
2022-05-01 14:35:23 +03:00
Saad Jutt
5aaac24080 fix: consume swagger api with CSRF 2022-05-01 06:07:17 +05:00
Saad Jutt
6d34206bbc chore(release): 0.0.64 2022-05-01 02:28:57 +05:00
Saad Jutt
7b39cc06d3 fix: removed fileExists for serving web 2022-05-01 02:28:50 +05:00
Saad Jutt
6e7f28a6f8 chore(release): 0.0.63 2022-05-01 02:10:24 +05:00
Saad Jutt
5689169ce4 chore: syntax fix for workflow 2022-05-01 02:10:17 +05:00
Saad Jutt
6139e7bff6 chore(release): 0.0.62 2022-05-01 02:08:03 +05:00
Saad Jutt
2c77317bb9 chore: release using node LTS 2022-05-01 02:07:55 +05:00
Saad Jutt
57b63db9cb chore(release): 0.0.61 2022-05-01 01:59:12 +05:00
Saad Jutt
60a2a4fe32 chore: bumped pkg version 2022-05-01 01:59:04 +05:00
Allan Bowe
09611cb416 chore(release): 0.0.60 2022-04-30 18:53:44 +00:00
Allan Bowe
2a9bb6e6b1 Merge pull request #155 from sasjs/api-access-via-session-authentication
fix: added CSRF check for granting access via session authentication
2022-04-30 21:44:35 +03:00
Saad Jutt
b4b60c69cf fix: setting CSRF Token for only rendering SPA 2022-04-30 06:32:24 +05:00
Saad Jutt
b060ad1b8e fix: added CSRF check for granting access via session authentication 2022-04-30 05:04:27 +05:00
munja
d47ed6d0e8 chore(release): 0.0.59 2022-04-29 13:28:34 +01:00
Muhammad Saad
a6993ef5ae Merge pull request #153 from sasjs/csrf-tokens-for-web
feat: enabled csrf tokens for web component
2022-04-28 17:26:05 -07:00
Saad Jutt
2571fc2ca8 chore: README.md updated 2022-04-29 05:09:11 +05:00
Saad Jutt
992f39b63a chore: lint fix 2022-04-29 04:11:29 +05:00
Saad Jutt
1ea3f6d8b3 chore: corrected order for web route 2022-04-29 03:11:08 +05:00
Saad Jutt
e462aebdc0 feat: enabled csrf tokens for web component 2022-04-29 02:59:48 +05:00
Muhammad Saad
13403517a4 Merge pull request #150 from sasjs/session-based-authentication
feat: enabled session based authentication for web
2022-04-28 07:11:45 -07:00
Saad Jutt
c3c2048e75 chore: temp 2022-04-28 07:15:36 +05:00
Saad Jutt
1d8acc36eb chore: temp 2022-04-28 07:15:09 +05:00
Saad Jutt
4c7ad56326 test: fixed specs 2022-04-28 07:07:56 +05:00
Saad Jutt
e57443f1ed fix(web): show display name instead of username 2022-04-28 07:00:49 +05:00
Saad Jutt
5da93f318a feat: enabled session based authentication for web 2022-04-28 06:44:25 +05:00
Muhammad Saad
a30fb1a241 Merge pull request #141 from sasjs/issue-135
fix: fetch client from DB for each request
2022-04-27 12:09:41 -07:00
Allan Bowe
4ae8f35e9a chore(release): 0.0.58 2022-04-24 20:25:08 +00:00
Saad Jutt
ebb46f51b6 chore: fix specs 2022-04-24 05:29:42 +05:00
Saad Jutt
fe24f51ca2 test: fix specs 2022-04-24 05:17:25 +05:00
Saad Jutt
fd15f3fb41 test: fix specs 2022-04-24 05:11:56 +05:00
Saad Jutt
7d31ee7696 chore: Merge branch 'main' into issue-135 2022-04-24 05:08:31 +05:00
Allan Bowe
667e26b080 Merge pull request #142 from sasjs/contribute
npm scripts updated
2022-04-24 02:48:07 +03:00
munja
d09876c05f fix: missing dependency 2022-04-24 00:46:14 +01:00
Saad Jutt
fb8e18be75 chore: fix vulnerabilites 2022-04-24 04:38:28 +05:00
Saad Jutt
7ac7a4e083 chore: added start:prod npm script 2022-04-24 04:36:42 +05:00
Allan Bowe
8e23786dd4 Update CONTRIBUTING.md 2022-04-24 00:31:41 +01:00
Allan Bowe
4bd01bcf29 Update CONTRIBUTING.md 2022-04-24 00:30:29 +01:00
Saad Jutt
4ad8c81e49 fix: fetch client from DB for each request 2022-04-24 04:16:13 +05:00
Allan Bowe
51f6aa34a1 Merge pull request #140 from sasjs/corebump
fix: bumping core library to get latest user management macros
2022-04-24 02:02:16 +03:00
Allan Bowe
486207128d fix: bumping core library to get latest user management macros 2022-04-23 22:54:34 +00:00
Allan Bowe
1e4b0b9171 Update README.md 2022-04-21 12:38:31 +01:00
Allan Bowe
1ff820605a chore(release): 0.0.57 2022-04-21 09:27:23 +00:00
Muhammad Saad
9c1a781b3a Merge pull request #136 from sasjs/issue-78
feat: add user name and logout functionality
2022-04-20 16:57:32 -07:00
36628551ae chore(web): use AppContext instead of useTokens Hook 2022-04-21 04:37:40 +05:00
23cf8fa06f chore(web): add user name at top right 2022-04-21 04:36:20 +05:00
84ee743eae feat: create AppContext 2022-04-21 04:34:27 +05:00
Allan Bowe
19e5bd7d2d chore: updating README with docs on linux jobs as approach for running server as background job 2022-04-20 13:16:06 +00:00
Allan Bowe
e251747302 chore(release): 0.0.56 2022-04-20 08:44:48 +00:00
Allan Bowe
7e7558d4cf Merge pull request #133 from sasjs/allanbowe/lengths-of-username-password-61
fix: shortening min length of username.  Closes #61
2022-04-20 11:44:15 +03:00
Allan Bowe
f02996facf fix: shortening min length of username. Closes #61 2022-04-20 08:43:38 +00:00
Saad Jutt
803c51f400 chore(release): 0.0.55 2022-04-20 07:15:33 +05:00
Muhammad Saad
c35b2b3f59 Merge pull request #132 from sasjs/fix-drive-path
Fix drive path
2022-04-19 18:56:13 -07:00
Saad Jutt
fe0866ace7 chore: Merge branch 'main' into fix-drive-path 2022-04-20 06:53:31 +05:00
Muhammad Saad
1513c3623d Merge pull request #131 from sasjs/fix-specs
test: fixed unhandled timeout
2022-04-19 18:52:52 -07:00
Saad Jutt
7fe43ae0b7 test: fixed unhandled timeout 2022-04-20 06:50:11 +05:00
Saad Jutt
c4cea4a12b fix: drive path in server mode 2022-04-20 05:54:56 +05:00
Saad Jutt
9fc7a132ba test: fixed unhandled timout 2022-04-20 00:06:21 +05:00
Allan Bowe
d55a619d64 chore(release): 0.0.54 2022-04-19 18:40:30 +00:00
Allan Bowe
737d2a24c2 Merge pull request #130 from sasjs/db-seed
fix: added db seed at server startup
2022-04-19 21:39:38 +03:00
Saad Jutt
2e63831b90 fix: added db seed at server startup 2022-04-19 23:25:05 +05:00
Saad Jutt
c7ffde1a3b chore(release): 0.0.53 2022-04-19 21:27:07 +05:00
Saad Jutt
db70b1ce55 fix: provide clientId to web component 2022-04-19 21:26:55 +05:00
Muhammad Saad
8a3fe8b217 Merge pull request #129 from sasjs/improve-UX-in-drive
fix: improve user experience in sasjs drive
2022-04-18 13:10:57 -07:00
9dca552e82 fix(drive):when page is refreshed or reloaded show expand file tree according to filePath in url 2022-04-19 00:40:37 +05:00
Allan Bowe
505f2089c7 chore(release): 0.0.52 2022-04-17 21:26:59 +00:00
Muhammad Saad
3344c400a8 Merge pull request #127 from sasjs/add-server-info-api-endpoint
feat: add api endpoint for getting server info
2022-04-17 12:48:12 -07:00
fa6248e3ef chore: swagger.yml updated 2022-04-17 23:53:20 +05:00
9fb5f1f8e7 feat: add api for getting server info 2022-04-17 23:48:08 +05:00
munja
92e0b8a088 chore(release): 0.0.51 2022-04-15 14:30:43 +01:00
Allan Bowe
b484306ed8 Merge pull request #126 from sasjs/issue-119
running code with CTRL+ENTER
2022-04-15 16:29:23 +03:00
5e08aacc51 chore: css fix 2022-04-15 14:53:36 +02:00
a9e4eb685d chore: style fix 2022-04-15 14:26:45 +02:00
31b09f27cc style: lint 2022-04-15 14:23:36 +02:00
9f3ec92f8e chore: run button style fix 2022-04-15 14:23:15 +02:00
6c9e449614 style: lint 2022-04-14 19:56:22 +02:00
68e84b0994 feat: run button running man, sub menu added 2022-04-14 19:38:44 +02:00
f0bb51a0d5 chore: placement of ctrl enter label 2022-04-13 22:12:40 +02:00
b93a0da3a3 feat: running code with CTRL+ENTER 2022-04-13 15:27:41 +02:00
Allan Bowe
e5facbf54c Update README.md 2022-04-13 12:24:42 +01:00
Allan Bowe
cb2bebbe76 Update README.md 2022-04-12 12:47:55 +01:00
Allan Bowe
9e1e0ce8cc chore(release): 0.0.50 2022-04-07 15:25:04 +00:00
Allan Bowe
29928753b7 Update CONTRIBUTING.md 2022-04-07 16:24:36 +01:00
Allan Bowe
edd69ecaae Merge pull request #122 from sasjs/issue121
Fixed couple of bugs + feature implemented
2022-04-07 18:23:34 +03:00
Saad Jutt
74ba65f9f3 feat(appstream): Upload an app from appStream page 2022-04-07 20:18:36 +05:00
Saad Jutt
f257602834 fix: web component UI fix for studio scrolling 2022-04-07 19:10:45 +05:00
Saad Jutt
61080d4694 fix: web component added tooltip for webout in studio 2022-04-07 18:59:31 +05:00
Saad Jutt
82633adbc4 chore: removed unused util 2022-04-07 18:48:31 +05:00
Saad Jutt
23db7e7b7d fix: session death time has to be a valid string number 2022-04-07 18:48:22 +05:00
Saad Jutt
cbaa687c9b chore(release): 0.0.49 2022-04-02 07:09:39 +05:00
Saad Jutt
527f70e90d fix(stp): read file in non-binary mode if debug one 2022-04-02 07:09:27 +05:00
Saad Jutt
122faad55f chore(release): 0.0.48 2022-04-02 07:06:18 +05:00
Saad Jutt
3ff6f5e865 fix(stp): return log+webout for debug on 2022-04-02 07:06:09 +05:00
Muhammad Saad
7d5128c0d6 Merge pull request #115 from sasjs/issue109
feat(deploy): new route added for deploy with build.json
2022-04-02 06:45:28 +05:00
Saad Jutt
e1ebbfd087 chore: increased file upload size to 100mb 2022-04-02 06:04:34 +05:00
Saad Jutt
e430bdb0d4 test(upload): spec updated for file upload exceeding limit 2022-04-02 05:51:24 +05:00
Saad Jutt
9d9769eef3 chore: increased file upload size to 100mb 2022-04-02 05:36:53 +05:00
Saad Jutt
9d167abe2a fix: remove uploaded build.json from temp folder in all cases 2022-04-02 05:29:34 +05:00
Saad Jutt
18d0604bdd feat(deploy): new route added for deploy with build.json 2022-04-02 05:23:25 +05:00
Saad Jutt
7b7bc6b778 chore: fix vulnerabilities 2022-03-31 01:54:40 +05:00
Saad Jutt
fb4f3442d5 chore(release): 0.0.47 2022-03-30 01:04:45 +05:00
Muhammad Saad
09d1b7d5d4 Merge pull request #114 from sasjs/show-contents-as-api-responded
fix: show contents as api responded
2022-03-30 01:04:29 +05:00
Saad Jutt
99839ae62f chore: fix vulnerabilities 2022-03-30 00:55:59 +05:00
Saad Jutt
f700561e1a fix(web): updated STUDIO log and webout 2022-03-30 00:55:38 +05:00
Saad Jutt
8b4b4b91ab chore(release): 0.0.46 2022-03-30 00:01:18 +05:00
Muhammad Saad
acb3ae0493 Merge pull request #113 from sasjs/issue-108
feat(drive): GET folder contents API added
2022-03-30 00:00:52 +05:00
Saad Jutt
f48aeb1b0b chore(release): 0.0.45 2022-03-29 23:38:09 +05:00
Saad Jutt
5c0e8e5344 chore: Merge branch 'main' into issue-108 2022-03-29 23:28:03 +05:00
Saad Jutt
0ac9e4af7d feat(drive): GET folder contents API added 2022-03-29 23:27:44 +05:00
Muhammad Saad
ee80f3f968 Merge pull request #112 from sasjs/allanbowe/lst-destination-needs-111
fix: providing a PRINT destination during SAS invocation.
2022-03-29 23:11:32 +05:00
Allan Bowe
7f4201ba85 fix: proving a PRINT destination during SAS invocation.
Closes #111
2022-03-29 16:11:15 +00:00
munja
f830bbc058 chore(release): 0.0.44 2022-03-29 11:25:00 +01:00
Allan Bowe
f8e1522a5a Merge pull request #110 from sasjs/issue-108
fix: increased req body size
2022-03-28 14:22:44 +03:00
Saad Jutt
0a5aeceab5 fix: DELETE req cannot have body 2022-03-28 05:05:03 +05:00
Saad Jutt
6dc39c0d91 fix: increased req body size 2022-03-28 04:53:24 +05:00
Saad Jutt
117a53ceea fix(session): increased session + bug fixed 2022-03-24 20:22:06 +05:00
Saad Jutt
dd56a95314 fix(stp): use same session from file upload 2022-03-24 18:06:28 +05:00
Saad Jutt
c5117abe71 chore: README.md updated 2022-03-24 05:56:08 +05:00
Saad Jutt
84c632a861 chore(release): 0.0.43 2022-03-24 04:25:40 +05:00
Muhammad Saad
3ddd09eba0 Merge pull request #105 from sasjs/deploy-app-with-current-names-also
Deploy app with current names also
2022-03-24 04:25:12 +05:00
Saad Jutt
0c0301433c test: fixed 2022-03-24 04:22:30 +05:00
Saad Jutt
954b2e3e2e chore: removed test file 2022-03-24 01:12:04 +05:00
Saad Jutt
5655311b96 fix: fallback logo on AppStream 2022-03-24 01:07:06 +05:00
Saad Jutt
9ace33d783 fix(deploy): user can deploy to same appName with different/same appLoc 2022-03-24 00:54:59 +05:00
Saad Jutt
adc5aca0f0 chore(release): 0.0.42 2022-03-23 22:49:29 +05:00
Muhammad Saad
71c6be6b84 Merge pull request #104 from sasjs/webout-raw
fix: execute api, webout as raw
2022-03-23 22:47:07 +05:00
Saad Jutt
9c751877d1 fix: execute api, webout as raw 2022-03-23 22:41:02 +05:00
Saad Jutt
2204d54cd6 chore(release): 0.0.41 2022-03-23 21:32:06 +05:00
Saad Jutt
f4eb75ff34 fix(scroll): closes #100 2022-03-23 21:31:54 +05:00
Saad Jutt
a3cde343b7 chore(release): 0.0.40 2022-03-23 20:17:15 +05:00
Saad Jutt
7a70d40dbf fix: macros available for SAS 2022-03-23 20:15:18 +05:00
Saad Jutt
d27e070fc8 fix: moved macros from codebase to drive
This reverts commit d2956fc641.
2022-03-23 19:38:29 +05:00
Saad Jutt
27e260e6a4 fix(deploy): validating empty file or service in filetree 2022-03-23 19:38:20 +05:00
Saad Jutt
2796db8ead chore(release): 0.0.39 2022-03-23 18:07:12 +05:00
Muhammad Saad
84f7c2ab89 Merge pull request #103 from sasjs/executable-macros-fix
Executable macros fix
2022-03-23 18:07:01 +05:00
Saad Jutt
e68090181a fix: included sasjs core macros at compile time 2022-03-23 18:05:03 +05:00
Saad Jutt
d2956fc641 Revert "fix: moved macros from codebase to drive"
This reverts commit 9ac3191891.
2022-03-23 17:59:06 +05:00
Saad Jutt
a701bb25e7 Revert "fix: quick fix for executables"
This reverts commit 9e53470947.
2022-03-23 17:58:18 +05:00
Saad Jutt
5758bcd392 chore(release): 0.0.38 2022-03-23 17:16:01 +05:00
Saad Jutt
9e53470947 fix: quick fix for executables 2022-03-23 17:15:46 +05:00
Saad Jutt
81f6605249 chore(release): 0.0.37 2022-03-23 09:23:20 +05:00
Muhammad Saad
0b45402946 Merge pull request #102 from sasjs/issue-95
fix: moved macros from codebase to drive
2022-03-23 09:23:01 +05:00
Saad Jutt
9ac3191891 fix: moved macros from codebase to drive 2022-03-23 09:19:33 +05:00
Saad Jutt
cd00aa2af8 fix: appStream html view 2022-03-22 21:52:39 +05:00
Saad Jutt
0147bcb701 fix(webin): closes #99 2022-03-22 21:28:31 +05:00
Saad Jutt
bf53ad30f4 chore(release): 0.0.36 2022-03-22 04:07:27 +05:00
Muhammad Saad
a003b8836b Merge pull request #98 from sasjs/issue-91
feat: App Stream
2022-03-22 04:02:26 +05:00
Saad Jutt
df6003df94 fix(appstream): app logo + improvements 2022-03-22 03:55:51 +05:00
Saad Jutt
b1d0fdbb02 chore(release): 0.0.35 2022-03-21 18:24:35 +05:00
Muhammad Saad
2c34395110 Merge pull request #97 from sasjs/issue-96
feat(cors): whitelisting is configurable through .env variables
2022-03-21 18:21:39 +05:00
Saad Jutt
534e4e5bf3 chore: README.md updated 2022-03-21 18:17:26 +05:00
Saad Jutt
6146372eba chore: README.md updated 2022-03-21 18:05:40 +05:00
Saad Jutt
aaa469a142 chore: .env.example updated 2022-03-21 17:54:20 +05:00
Saad Jutt
4fd5bf948e fix(cors): removed trailing slashes of urls 2022-03-21 17:49:28 +05:00
Saad Jutt
99f91fbce2 feat(cors): whitelisting is configurable through .env variables 2022-03-21 17:36:42 +05:00
Saad Jutt
98a00ec7ac feat: App Stream, load on startup, new route added 2022-03-21 17:17:29 +05:00
munja
b0fb858c49 chore(release): 0.0.34 2022-03-18 23:37:37 +00:00
Allan Bowe
83959ef99e Update README.md 2022-03-18 23:36:15 +00:00
Allan Bowe
08087495d3 Merge pull request #94 from sasjs/add-license-1
Create LICENSE
2022-03-18 23:53:05 +02:00
Allan Bowe
3f68474839 Create LICENSE 2022-03-18 14:44:00 +00:00
Allan Bowe
f26886f84d Merge pull request #93 from sasjs/build-files
chore: copy other files as part of npm run build
2022-03-18 16:42:09 +02:00
Allan Bowe
ddd50eac8e Merge pull request #92 from sasjs/issue-90
fix(stp): write original file name in sas code for upload
2022-03-18 16:41:45 +02:00
Saad Jutt
bba3e8d272 chore: reduced stp sample data 2022-03-18 16:30:54 +05:00
Saad Jutt
30944bfa18 chore: copy other files as part of npm run build 2022-03-18 16:24:34 +05:00
Saad Jutt
8822de95df fix(stp): write original file name in sas code for upload 2022-03-18 06:57:52 +05:00
Allan Bowe
02a242fe4b Merge pull request #88 from sasjs/quick-fix
fix: web index js script included
2022-03-17 11:55:03 +02:00
Allan Bowe
1beac914db Merge pull request #89 from sasjs/remove-deleted-file
fix: remove deleted file from directory tree in sidebar of drive
2022-03-17 11:54:35 +02:00
Saad Jutt
a45b42107e chore: fixed specs 2022-03-17 07:22:53 +05:00
3d89b753f0 feat(web): directory tree in sidebar of drive should be expanded by default at root level 2022-03-16 19:13:50 +00:00
fb77d99177 fix(web-drive): upon delete remove entry of deleted file from directory tree in sidebar 2022-03-16 18:52:00 +00:00
fa627aabf9 chore(web-refactor): state uplifted for drive container in web component 2022-03-16 18:48:18 +00:00
Saad Jutt
fd2629862f fix: preferred to show param errors from query 2022-03-16 19:21:39 +05:00
Saad Jutt
75291f9397 fix: desktop mode web index.html js script included 2022-03-16 19:20:38 +05:00
Saad Jutt
99fb5f4b2b chore(release): 0.0.33 2022-03-16 06:44:35 +05:00
Muhammad Saad
5dc3deeb11 Merge pull request #86 from sasjs/issue-80
Issue 80
2022-03-16 06:24:41 +05:00
Saad Jutt
6b708fcad3 fix: added api button on web component 2022-03-16 06:22:26 +05:00
Saad Jutt
bc0ff84d8d chore: updated code as per sasjs/utils breaking change 2022-03-16 05:04:33 +05:00
Saad Jutt
1ff6965dd2 fix: adde validation + code improvement 2022-03-16 04:53:07 +05:00
Saad Jutt
d6fa877941 feat: serve deployed streaming apps 2022-03-15 03:54:19 +05:00
Saad Jutt
940f705f5d chore(release): 0.0.32 2022-03-14 06:26:38 +05:00
Saad Jutt
7a6e6c8bec feat(web): added delete option in Drive 2022-03-14 06:26:12 +05:00
Saad Jutt
67d200d817 chore(release): 0.0.31 2022-03-14 05:36:24 +05:00
Muhammad Saad
a0c27ea8d3 Merge pull request #83 from sasjs/issue-52
fix: added cookie for accessToken
2022-03-14 05:35:59 +05:00
Saad Jutt
3d583ff21d feat(drive): new route delete file api 2022-03-14 05:30:10 +05:00
Saad Jutt
7072e282b1 fix(drive): update file API is same as create file 2022-03-14 05:05:59 +05:00
Saad Jutt
145ac45036 fix(stp): return plain/text header for GET & debug 2022-03-14 04:42:30 +05:00
Saad Jutt
698180ab7e fix: added cookie for accessToken 2022-03-09 05:06:06 +05:00
Muhammad Saad
0f4e38d51d Merge pull request #81 from sasjs/issue-79
fix: show content of get file api
2022-03-07 01:29:40 +05:00
Saad Jutt
e76283daa4 chore: few improvements 2022-03-06 18:46:09 +05:00
Saad Jutt
6ab42ca486 fix: show content of get file api 2022-03-06 18:45:07 +05:00
Saad Jutt
fa4da7624b chore(release): 0.0.30 2022-03-06 03:02:37 +05:00
Muhammad Saad
9f5509d2d4 Merge pull request #77 from sasjs/issue-65
fix: macros are available Sessions with SASAUTOS
2022-03-06 02:44:38 +05:00
Saad Jutt
efaf38d303 fix: get file instead of it's content 2022-03-06 02:33:56 +05:00
Saad Jutt
95843fa4c7 fix: macros are available Sessions with SASAUTOS 2022-03-06 01:57:14 +05:00
Muhammad Saad
5ba7661a83 Merge pull request #75 from sasjs/improve-file-upload
Improve file upload
2022-03-02 18:56:28 +05:00
Saad Jutt
ed5c58e10e chore: added restclient sample requests 2022-02-28 23:39:03 +05:00
Saad Jutt
5fce7d8f71 chore: added some docs to file upload 2022-02-28 22:54:49 +05:00
Saad Jutt
feeec4eb14 fix(upload): added query param as well for filepath 2022-02-28 22:34:18 +05:00
Saad Jutt
8c1941a87b fix: improvement in flow of uploading 2022-02-28 22:12:39 +05:00
Muhammad Saad
765969db11 Merge pull request #74 from sasjs/issue-67
File uploading multi-part
2022-02-28 04:15:18 +05:00
Saad Jutt
e60f17268d fix: multi-part file upload + validations + specs 2022-02-28 04:06:13 +05:00
Saad Jutt
ce0a5e1229 fix: organized code for usage of multer 2022-02-28 04:04:30 +05:00
Saad Jutt
c5738792b0 chore: debugger for api 2022-02-28 04:03:00 +05:00
Muhammad Saad
94e036dd10 Merge pull request #73 from sasjs/issue-72
fix(stp): return json for webout
2022-02-26 02:46:07 +05:00
Saad Jutt
da375b8086 chore: lint fixes 2022-02-26 02:41:55 +05:00
Allan Bowe
7312763339 fix: updating docs 2022-02-22 19:35:58 +00:00
Saad Jutt
5005f203b8 fix(stp): return json for webout 2022-02-21 04:13:04 +05:00
Muhammad Saad
232a73fd17 Merge pull request #71 from sasjs/return-file-in-response
Return file in response
2022-02-20 05:24:48 +04:00
Saad Jutt
ef41691e40 fix(file): fixes response headers 2022-02-20 06:18:44 +05:00
Saad Jutt
3e6234e601 fix: return buffer in case of file response 2022-02-20 05:40:03 +05:00
Saad Jutt
0a4b202428 fix: hot fix for web component 2022-02-20 05:03:48 +05:00
Muhammad Saad
a11893ece1 Merge pull request #70 from sasjs/parse-log-to-array
feat: parse log to array
2022-02-20 03:54:19 +04:00
Saad Jutt
c5ad72c931 feat: parse log to array 2022-02-20 04:50:09 +05:00
Muhammad Saad
034f3173bd Merge pull request #69 from sasjs/route-code-fixes
fix: code api is updated return type
2022-02-20 03:13:19 +04:00
Saad Jutt
e2a6810e95 fix: code api is updated return type 2022-02-20 02:57:45 +05:00
Muhammad Saad
373d66f8af Merge pull request #66 from sasjs/set-response-headers
Set response headers
2022-02-18 07:08:45 +04:00
Saad Jutt
0b5f958f45 fix: minor changes 2022-02-18 08:03:01 +05:00
Saad Jutt
da899b90e2 fix: added http headers to /code api as well 2022-02-18 07:25:48 +05:00
Saad Jutt
2c4aa420b3 feat: set response headers provded by SAS Code execution 2022-02-18 07:22:11 +05:00
munja
cd32912379 chore: updating readme with env config options 2022-02-16 22:00:36 +00:00
munja
93dcb1753b chore(release): 0.0.29 2022-02-16 21:43:01 +00:00
Allan Bowe
35cf301905 Merge pull request #63 from sasjs/issue58
fix: adding .. in folder path
2022-02-16 23:42:45 +02:00
Allan Bowe
5931fc1e71 fix: adding .. in folder path 2022-02-16 21:41:25 +00:00
Allan Bowe
18d845799c Merge pull request #59 from sasjs/issue58
fix: adding sasjs stpsrv_header() path to autoexec.  Relates to #58
2022-02-16 23:22:53 +02:00
munja
8c872bde92 chore(release): 0.0.28 2022-02-16 21:14:53 +00:00
Allan Bowe
f953472efd Merge pull request #62 from sasjs/headerfix
feat: default macros and bumping core
2022-02-16 23:07:31 +02:00
munja
f10138b0f2 fix: moving core 2022-02-16 21:05:22 +00:00
munja
6f19d3d0ea feat: default macros and bumping core 2022-02-16 21:03:16 +00:00
munja
a7facb005a chore: updating README with correct clientid 2022-02-16 14:52:56 +00:00
munja
88acf9df5d chore(release): 0.0.27 2022-02-16 14:00:34 +00:00
Allan Bowe
b0880b142a Merge pull request #60 from sasjs/errfix
feat: removing stpsrv_header and updating README with auth details
2022-02-16 15:59:56 +02:00
munja
d3674c7f94 feat: removing stpsrv_header and updating README with auth details 2022-02-16 13:19:50 +00:00
Saad Jutt
adccca6c7f chore: updated README.md 2022-02-15 21:53:54 +05:00
Yury Shkoda
8b83ccc4c2 Merge pull request #51 from sasjs/cli-issue-1108
feat: return json response with the log ob job execution
2022-02-15 11:08:17 +03:00
Yury Shkoda
556944b1d5 chore(git): Merge remote-tracking branch 'origin/main' into cli-issue-1108 2022-02-15 10:25:38 +03:00
Saad Jutt
b14e07ee6e chore(release): 0.0.26 2022-02-15 04:09:55 +05:00
Muhammad Saad
048bd9f78c Merge pull request #57 from sasjs/final-release-should-also-has-https-server
fix: release should also has https protocol
2022-02-15 03:07:17 +04:00
Saad Jutt
d7e1aca7e3 fix: refactored + removed unused package 2022-02-15 04:04:38 +05:00
Saad Jutt
de47d78a00 chore: Merge branch 'main' into final-release-should-also-has-https-server 2022-02-15 03:12:28 +05:00
JahanzaibRao15
58b6f439b3 Fixed vulnerabilities and remove unused or redundant dependencies (#55)
* chore: fix vulnerabilities and remove unused or redundant dependencies

* chore: Added start script and vulnerabilities screenshot

* fix: quick fix

* fix: converted to typescript + bugs removed

Co-authored-by: jahanzaibrao-dev <jahanzaib@tetrahex.com>
Co-authored-by: Saad Jutt <ihsan.distranger@gmail.com>
2022-02-15 03:11:03 +05:00
munja
ce9bde5717 fix: adding sasjs stpsrv_header() path to autoexec. Relates to #58 2022-02-14 18:49:35 +00:00
Saad Jutt
0cfe724ffa fix: release should also has https protocol 2022-02-14 19:12:37 +05:00
Yury Shkoda
fde4bc051d chore(execution): roll back changes related to returnLog var 2022-02-14 15:43:56 +03:00
Muhammad Saad
367b0f1f89 Merge pull request #54 from sasjs/token-expiry-updated
fix: updated token expiry times
2022-02-11 21:05:31 +04:00
Saad Jutt
d17a3dd590 fix: updated token expiry times 2022-02-11 21:54:27 +05:00
munja
bee5deed2a chore(release): 0.0.25 2022-02-11 17:30:39 +01:00
Muhammad Saad
e6e46838b3 Merge pull request #53 from sasjs/corebump
fix: adding global macvar and bumping sasjs/core with additional server support
2022-02-11 20:28:16 +04:00
munja
404f1ec059 fix: adding global macvar and bumping sasjs/core with additional server support 2022-02-11 17:24:55 +01:00
munja
09d36bc754 chore(release): 0.0.24 2022-02-11 12:53:06 +01:00
Muhammad Saad
3722bbaec3 Merge pull request #52 from sasjs/forever
chore(readme): forever package
2022-02-11 15:43:23 +04:00
munja
480ee4da83 fix: removing sysmacdelete 2022-02-11 12:11:34 +01:00
munja
dd853fe13b chore(docs): adding contributing 2022-02-11 12:05:05 +01:00
munja
e1142a33a0 chore: automated commit 2022-02-11 11:27:47 +01:00
munja
d4e8d91cae chore(readme): forever package 2022-02-11 10:47:40 +01:00
Saad Jutt
9a74ec545d chore: docker fix for SAS executable 2022-02-11 14:30:25 +05:00
Yury Shkoda
f2000a1227 chore: fix typos and remove unused code 2022-02-10 09:07:14 +03:00
Yury Shkoda
bf5767eadf feat(stp-execution): add returnLog option to execution query 2022-02-10 09:06:29 +03:00
Saad Jutt
e3f5206758 chore(release): 0.0.23 2022-02-08 21:46:00 +05:00
Saad Jutt
fffd21b348 chore: quick fixes 2022-02-08 21:45:56 +05:00
Saad Jutt
2d74ef5e12 chore(release): 0.0.22 2022-02-08 21:45:41 +05:00
munja
224743a439 Merge branch 'main' of github.com:sasjs/server 2022-02-01 15:19:56 +01:00
munja
f39a76da17 chore(release): 0.0.21 2022-02-01 15:19:36 +01:00
Allan Bowe
6107d02c8e Merge pull request #49 from sasjs/autoexecfix
fix: adding missing global vars to autoexec
2022-02-01 16:18:38 +02:00
munja
1966b17f27 fix: adding missing global vars to autoexec 2022-02-01 15:14:31 +01:00
Allan Bowe
87c8aa5146 Merge pull request #47 from sasjs/fixnot
fix: avoid uninitialised note
2022-01-26 18:30:21 +02:00
munja
e4c027ad51 fix: avoid uninitialised note 2022-01-26 17:27:08 +01:00
munja
083355fdba chore(release): 0.0.20 2022-01-20 14:03:05 +01:00
munja
a3b57f6e28 fix: fixing versioning blooper 2022-01-20 14:03:00 +01:00
munja
b0ffa145bc chore(release): 0.0.2 2022-01-20 13:57:35 +01:00
munja
a8df5f4afd fix: bumping core version 2022-01-20 13:57:31 +01:00
munja
62de960e86 chore(release): 0.0.19 2022-01-20 10:39:34 +01:00
munja
31532c0efa fix: bumping sasjs/core and updating descriptions 2022-01-20 10:37:22 +01:00
Allan Bowe
732230524d Merge pull request #46 from sasjs/allanbowe-patch-1
Update README.md
2022-01-20 11:06:25 +02:00
Allan Bowe
6dc281313e Update README.md 2022-01-19 22:34:41 +00:00
munja
92db3c7c82 chore(release): 0.0.18 2022-01-08 20:24:57 +01:00
munja
d8b75a47d3 fix: compressing release files for faster download times 2022-01-08 20:22:22 +01:00
Saad Jutt
d70fc1032f chore(release): 0.0.17 2022-01-08 02:15:38 +05:00
Muhammad Saad
794ee8f6e0 Merge pull request #45 from sasjs/hot-fix
fix: bug removed, log is clean now
2022-01-08 01:11:04 +04:00
Saad Jutt
43769e711d fix: bug removed, log is clean now 2022-01-08 00:42:32 +05:00
Saad Jutt
30528a1528 chore(release): 0.0.16 2022-01-07 16:47:01 +05:00
Muhammad Saad
b7e1753d25 Merge pull request #44 from sasjs/cors-update
Cors update
2022-01-07 15:46:24 +04:00
Saad Jutt
9c5772a303 chore: clean up 2022-01-07 16:42:38 +05:00
Saad Jutt
7a3d710153 fix: session should be marked as consumed 2022-01-07 16:34:46 +05:00
Saad Jutt
0a6ebe6e62 chore: debugging 2022-01-07 15:38:11 +05:00
Saad Jutt
6cbc657da3 fix: recreate crashed session 2022-01-06 23:12:44 +05:00
Saad Jutt
cd838915fd fix: added sas9 server address 2022-01-06 18:40:23 +05:00
Saad Jutt
4e486fda69 chore(release): 0.0.15 2022-01-06 17:23:29 +05:00
Muhammad Saad
79cac53fdb Merge pull request #42 from sasjs/run-sas-code-route
Run sas code route
2022-01-06 16:23:07 +04:00
Saad Jutt
450d99f06e fix(web): sticky tabs on Studio + extra run code button removed 2022-01-05 17:41:17 +05:00
Saad Jutt
51ee8c0825 fix(web): autosave and autofocus 2021-12-30 12:56:23 +05:00
Saad Jutt
a1151606f2 fix(web): parsing of webout 2021-12-30 12:18:48 +05:00
Saad Jutt
38193c83dd chore: Merge branch 'main' into run-sas-code-route 2021-12-30 12:18:16 +05:00
Muhammad Saad
59ecc36f2b Merge pull request #43 from sasjs/allanbowe-patch-1
Update home.tsx
2021-12-30 12:07:14 +05:00
Allan Bowe
8bc459c9a7 Update home.tsx 2021-12-29 19:15:21 +00:00
Saad Jutt
f1f1e47f76 chore(web): display webout as well 2021-12-29 01:00:53 +05:00
Saad Jutt
679e9de245 chore: return webout and log seperately 2021-12-29 00:27:54 +05:00
Saad Jutt
f0ac996b3c chore(web): build fix 2021-12-28 22:18:18 +05:00
Saad Jutt
2d77222ae8 fix(studio): web component updated 2021-12-28 22:02:11 +05:00
Saad Jutt
e6e5a5fd64 chore(code): updated route to code/execute 2021-12-21 14:24:27 +05:00
Saad Jutt
e1eb04494a fix: updated route for sas code 2021-12-21 12:36:58 +05:00
Allan Bowe
b7fa8e5f80 Merge pull request #40 from sasjs/fix-specs
chore: added driveLoc for specs
2021-12-20 11:39:55 +02:00
Saad Jutt
ef4fae4496 chore: added driveLoc for specs 2021-12-20 10:47:47 +05:00
munja
3e5a4e0555 chore(release): 0.0.14 2021-12-19 11:35:08 +00:00
Allan Bowe
cf9a8091ea Merge pull request #39 from sasjs/master
chore: updating package.json
2021-12-19 13:33:41 +02:00
munja
0edc45dd0a chore: updating package.json 2021-12-19 11:32:48 +00:00
munja
ceca370e27 fix: switch to main branch 2021-12-19 11:30:27 +00:00
Allan Bowe
f235b9c2f9 Merge pull request #38 from sasjs/weboutfix
fix: actually a README change, the fix was in the previous commit (up…
2021-12-18 23:24:57 +02:00
Allan Bowe
d86c841f1f fix: actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release 2021-12-18 20:31:40 +00:00
Allan Bowe
076b866c02 fix: bumping sasjs/core with adjustment to ms_webout() 2021-12-18 20:26:07 +00:00
Saad Jutt
19d4430b31 chore(release): 0.0.13 2021-12-16 16:13:05 +05:00
Saad Jutt
e5be0e6789 fix: output for Studio 2021-12-16 12:44:57 +05:00
Saad Jutt
27129a8921 feat(studio): run selected code + open in studio 2021-12-16 12:14:32 +05:00
Saad Jutt
da11c03d55 chore: sasjs/core version bumped 2021-12-15 19:57:09 +05:00
Saad Jutt
4fbdda0365 chore: .env.example updated 2021-12-15 18:58:04 +05:00
Saad Jutt
efacb1e916 chore(release): 0.0.12 2021-12-15 18:24:17 +05:00
Saad Jutt
d19ce253b4 fix: use env if provided for desktop mode 2021-12-15 18:24:04 +05:00
Saad Jutt
e11a4b66e7 chore(release): 0.0.11 2021-12-15 17:51:42 +05:00
Muhammad Saad
d0a1457f44 feat: added authorization route for web (#37) 2021-12-15 17:51:19 +05:00
Saad Jutt
34e54934fd chore(release): 0.0.10 2021-12-07 16:04:07 +05:00
Allan Bowe
4873e6054f Merge pull request #36 from sasjs/check-session-token-validity
chore: added check session + code refactor
2021-12-07 08:49:00 +00:00
Saad Jutt
b00aa4e17b chore: additional info on SAS session crash 2021-12-07 10:37:36 +05:00
Saad Jutt
9fccfe6f35 chore: Merge branch 'master' into check-session-token-validity 2021-12-07 10:37:02 +05:00
Saad Jutt
0a9d734e09 chore(release): 0.0.9 2021-12-07 10:35:56 +05:00
Muhammad Saad
a0822e6b61 fix: release with files (#35) 2021-12-07 10:33:11 +05:00
Saad Jutt
43545fa04b chore: added check session + code refactor 2021-12-05 08:17:54 +05:00
Saad Jutt
a80e5c8ead chore(release): 0.0.3 2021-11-30 16:32:52 +05:00
Saad Jutt
c8634953ca chore: trigger release 2021-11-30 16:29:30 +05:00
Saad Jutt
129ad71a15 chore: trigger release 2021-11-30 16:16:17 +05:00
164 changed files with 29121 additions and 39552 deletions

84
.all-contributorsrc Normal file
View File

@@ -0,0 +1,84 @@
{
"projectName": "server",
"projectOwner": "sasjs",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": true,
"commitConvention": "angular",
"contributors": [
{
"login": "saadjutt01",
"name": "Saad Jutt",
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
"profile": "https://github.com/saadjutt01",
"contributions": [
"code",
"test"
]
},
{
"login": "sabhas",
"name": "Sabir Hassan",
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
"profile": "https://github.com/sabhas",
"contributions": [
"code",
"test"
]
},
{
"login": "YuryShkoda",
"name": "Yury Shkoda",
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
"profile": "https://www.erudicat.com/",
"contributions": [
"code",
"test"
]
},
{
"login": "medjedovicm",
"name": "Mihajlo Medjedovic",
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
"profile": "https://github.com/medjedovicm",
"contributions": [
"code",
"test"
]
},
{
"login": "allanbowe",
"name": "Allan Bowe",
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
"profile": "https://4gl.io/",
"contributions": [
"code",
"doc"
]
},
{
"login": "VladislavParhomchik",
"name": "Vladislav Parhomchik",
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
"profile": "https://github.com/VladislavParhomchik",
"contributions": [
"test"
]
},
{
"login": "kknapen",
"name": "Koen Knapen",
"avatar_url": "https://avatars.githubusercontent.com/u/78609432?v=4",
"profile": "https://github.com/kknapen",
"contributions": [
"userTesting"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}

View File

@@ -1,4 +1,5 @@
SAS_EXEC=<path to folder containing SAS executable 'sas'>
SAS_EXEC_PATH=<path to folder containing SAS executable>
SAS_EXEC_NAME=<name of SAS executable file>
PORT_API=<port for sasjs server (api)>
PORT_WEB=<port for sasjs web component(react)>
ACCESS_TOKEN_SECRET=<secret>

114
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,114 @@
# CONTRIBUTING
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
The app can be deployed using Docker or NodeJS.
## Configuration
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
* `.env` - the root .env file is used only for Docker deploys.
* `api/.env` - this is the primary file used in NodeJS deploys
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
## Using Docker
### Docker Development Mode
Command to run docker for development:
```
docker-compose up -d
```
It uses default docker compose file i.e. `docker-compose.yml` present at root.
It will build following images if running first time:
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
### Docker Production Mode
Command to run docker for production:
```
docker-compose -f docker-compose.prod.yml up -d
```
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
It will build following images if running first time:
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
## Using NodeJS:
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
### NodeJS Development Mode
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
### NodeJS Dev - Single Port
Here the environment variables should be configured under `api.env`. Then:
```
cd ./web && npm i && npm build
cd ../api && npm i && npm start
```
### NodeJS Dev - Seperate Ports
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
#### API server
```
cd api
npm install
npm start
```
#### Web Server
```
cd web
npm install
npm start
```
#### NodeJS Production Mode
Update the `.env` file in the *api* folder. Then:
```
npm run server
```
This will install/build `web` and install `api`, then start prod server.
## Executables
In order to generate the final executables:
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm run exe
```
This will install/build web app and install/create executables of sasjs server at root `./executables`
## Releases
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)

View File

@@ -54,6 +54,10 @@ jobs:
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
SESSION_SECRET: ${{secrets.SESSION_SECRET}}
RUN_TIMES: 'sas,js'
SAS_PATH: '/some/path/to/sas'
NODE_PATH: '/some/path/to/node'
- name: Build Package
working-directory: ./api

View File

@@ -3,15 +3,25 @@ name: SASjs Server Executable Release
on:
push:
branches:
- master
- main
jobs:
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies WEB
working-directory: ./web
run: npm ci
@@ -32,16 +42,18 @@ jobs:
env:
CI: true
- name: Create Tag with Release
uses: Klemensas/action-autotag@stable
with:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Compress Executables
working-directory: ./executables
run: |
zip linux.zip api-linux
zip macos.zip api-macos
zip windows.zip api-win.exe
- name: Install Semantic Release and plugins
run: |
npm i
npm i -g semantic-release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
./executables/api-linux
./executables/api-macos
./executables/api-win.exe
token: ${{ secrets.GH_TOKEN }}
run: |
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release

3
.gitignore vendored
View File

@@ -4,9 +4,12 @@ node_modules/
.DS_Store
.env*
sas/
sasjs_root/
tmp/
build/
sasjsbuild/
sasjscore/
certificates/
executables/
.env
api/csp.config.json

10
.gitpod.yml Normal file
View File

@@ -0,0 +1,10 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others.
tasks:
- init: npm install
vscode:
extensions:
- dbaeumer.vscode-eslint
- sasjs.sasjs-for-vscode

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16.15.1

43
.releaserc Normal file
View File

@@ -0,0 +1,43 @@
{
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md"
]
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "./executables/linux.zip",
"label": "Linux Executable Binary"
},
{
"path": "./executables/macos.zip",
"label": "Macos Executable Binary"
},
{
"path": "./executables/windows.zip",
"label": "Windows Executable Binary"
}
]
}
],
[
"@semantic-release/exec",
{
"publishCmd": "echo 'publish command'"
}
]
]
}

933
CHANGELOG.md Normal file
View File

@@ -0,0 +1,933 @@
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)
### Bug Fixes
* add runtimes to global process object ([194eaec](https://github.com/sasjs/server/commit/194eaec7d4a561468f83bf6efce484909ee532eb))
* code fixes for executing program from program path including file extension ([53854d0](https://github.com/sasjs/server/commit/53854d001279462104b24c0e59a8c94ab4938a94))
* code/execute controller logic to handle different runtimes ([23b6692](https://github.com/sasjs/server/commit/23b6692f02e4afa33c9dc95d242eb8645c19d546))
* convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram ([c58666e](https://github.com/sasjs/server/commit/c58666eb81514de500519e7b96c1981778ec149b))
* no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code ([9d5a5e0](https://github.com/sasjs/server/commit/9d5a5e051fd821295664ddb3a1fd64629894a44c))
* pass _program to execute file without extension ([5df619b](https://github.com/sasjs/server/commit/5df619b3f63571e8e326261d8114869d33881d91))
* refactor code for session selection in preUploadMiddleware function ([b444381](https://github.com/sasjs/server/commit/b4443819d42afecebc0f382c58afb9010d4775ef))
* refactor code in executeFile method of session controller ([dffe6d7](https://github.com/sasjs/server/commit/dffe6d7121d569e5c7d13023c6ca68d8c901c88e))
* refactor code in preUploadMiddleware function ([6d6bda5](https://github.com/sasjs/server/commit/6d6bda56267babde7b98cf69e32973d56d719f75))
* refactor sas/js session controller classes to inherit from base session controller class ([2c704a5](https://github.com/sasjs/server/commit/2c704a544f4e31a8e8e833a9a62ba016bcfa6c7c))
* **Studio:** style fix for runtime dropdown ([9023cf3](https://github.com/sasjs/server/commit/9023cf33b5fa4b13c2d5e9b80ae307df69c7fc02))
### Features
* configure child process with writeStream to write logs to log file ([058b3b0](https://github.com/sasjs/server/commit/058b3b00816e582e143953c2f0b8330bde2181b8))
* conver single session controller to two controller i.e. SASSessionController and JSSessionController ([07295aa](https://github.com/sasjs/server/commit/07295aa151175db8c93eeef806fc3b7fde40ac72))
* create and inject code for uploaded files to code.js ([1685616](https://github.com/sasjs/server/commit/16856165fb292dc9ffa897189ba105bd9f362267))
* validate sasjs_runtimes env var ([596ada7](https://github.com/sasjs/server/commit/596ada7ca88798d6d71f6845633a006fd22438ea))
## [0.6.1](https://github.com/sasjs/server/compare/v0.6.0...v0.6.1) (2022-06-17)
### Bug Fixes
* home page wording. Using fix to force previous change through.. ([8702a4e](https://github.com/sasjs/server/commit/8702a4e8fd1bbfaf4f426b75e8b85a87ede0e0b0))
# [0.6.0](https://github.com/sasjs/server/compare/v0.5.0...v0.6.0) (2022-06-16)
### Features
* get group by group name ([6b0b94a](https://github.com/sasjs/server/commit/6b0b94ad38215ae58e62279a4f73ac3ed2d9d0e8))
# [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16)
### Bug Fixes
* npm audit fix to avoid warnings on npm i ([28a6a36](https://github.com/sasjs/server/commit/28a6a36bb708b93fb5c2b74d587e9b2e055582be))
### Features
* **api:** deployment through zipped/compressed file ([b81d742](https://github.com/sasjs/server/commit/b81d742c6c70d4cf1cab365b0e3efc087441db00))
## [0.4.2](https://github.com/sasjs/server/compare/v0.4.1...v0.4.2) (2022-06-15)
### Bug Fixes
* appStream redesign ([73792fb](https://github.com/sasjs/server/commit/73792fb574c90bd280c4324e0b41c6fee7d572b6))
## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15)
### Bug Fixes
* add/remove group to User when adding/removing user from group and return group membership on getting user ([e08bbcc](https://github.com/sasjs/server/commit/e08bbcc5435cbabaee40a41a7fb667d4a1f078e6))
# [0.4.0](https://github.com/sasjs/server/compare/v0.3.10...v0.4.0) (2022-06-14)
### Features
* new APIs added for GET|PATCH|DELETE of user by username ([aef411a](https://github.com/sasjs/server/commit/aef411a0eac625c33274dfe3e88b6f75115c44d8))
## [0.3.10](https://github.com/sasjs/server/compare/v0.3.9...v0.3.10) (2022-06-14)
### Bug Fixes
* correct syntax for encoding option ([32d372b](https://github.com/sasjs/server/commit/32d372b42fbf56b6c0779e8f704164eaae1c7548))
## [0.3.9](https://github.com/sasjs/server/compare/v0.3.8...v0.3.9) (2022-06-14)
### Bug Fixes
* forcing utf 8 encoding. Closes [#76](https://github.com/sasjs/server/issues/76) ([8734489](https://github.com/sasjs/server/commit/8734489cf014aedaca3f325e689493e4fe0b71ca))
## [0.3.8](https://github.com/sasjs/server/compare/v0.3.7...v0.3.8) (2022-06-13)
### Bug Fixes
* execution controller better error handling ([8a617a7](https://github.com/sasjs/server/commit/8a617a73ae63233332f5788c90f173d6cd5e1283))
* execution controller error details ([3fa2a7e](https://github.com/sasjs/server/commit/3fa2a7e2e32f90050f6b09e30ce3ef725eb0b15f))
## [0.3.7](https://github.com/sasjs/server/compare/v0.3.6...v0.3.7) (2022-06-08)
### Bug Fixes
* **appstream:** redirect to relative + nested resource should be accessed ([5ab35b0](https://github.com/sasjs/server/commit/5ab35b02c4417132dddb5a800982f31d0d50ef66))
## [0.3.6](https://github.com/sasjs/server/compare/v0.3.5...v0.3.6) (2022-06-02)
### Bug Fixes
* **appstream:** should serve only new files for same app stream name with new deployment ([e6d1989](https://github.com/sasjs/server/commit/e6d1989847761fbe562d7861ffa0ee542839b125))
## [0.3.5](https://github.com/sasjs/server/compare/v0.3.4...v0.3.5) (2022-05-30)
### Bug Fixes
* bumping sasjs/core library ([61815f8](https://github.com/sasjs/server/commit/61815f8ae18be132e17c199cd8e3afbcc2fa0b60))
## [0.3.4](https://github.com/sasjs/server/compare/v0.3.3...v0.3.4) (2022-05-30)
### Bug Fixes
* **web:** system username for DESKTOP mode ([a8ba378](https://github.com/sasjs/server/commit/a8ba378fd1ff374ba025a96fdfae5c6c36954465))
## [0.3.3](https://github.com/sasjs/server/compare/v0.3.2...v0.3.3) (2022-05-30)
### Bug Fixes
* usage of autoexec API in DESKTOP mode ([12d424a](https://github.com/sasjs/server/commit/12d424acce8108a6f53aefbac01fddcdc5efb48f))
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)
### Bug Fixes
* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](https://github.com/sasjs/server/commit/2c259fe1de95d84e6929e311aaa6b895e66b42a3))
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
### Bug Fixes
* **api:** username should be lowercase ([5ad6ee5](https://github.com/sasjs/server/commit/5ad6ee5e0f5d7d6faa45b72215f1d9d55cfc37db))
* **web:** reduced width for autoexec input ([7d11cc7](https://github.com/sasjs/server/commit/7d11cc79161e5a07f6c5392d742ef6b9d8658071))
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)
### Features
* **web:** added profile + edit + autoexec changes ([c275db1](https://github.com/sasjs/server/commit/c275db184e874f0ee3a4f08f2592cfacf1e90742))
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)
### Bug Fixes
* **autoexec:** usage in case of desktop from file ([79dc2db](https://github.com/sasjs/server/commit/79dc2dba23dc48ec218a973119392a45cb3856b5))
### Features
* **api:** added autoexec + major type setting changes ([2a7223a](https://github.com/sasjs/server/commit/2a7223ad7d6b8f3d4682447fd25d9426a7c79ac3))
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)
### Bug Fixes
* issue174 + issue175 + issue146 ([80b33c7](https://github.com/sasjs/server/commit/80b33c7a18c1b7727316ffeca71658346733e935))
* **web:** click to copy + notification ([f37f8e9](https://github.com/sasjs/server/commit/f37f8e95d1a85e00ceca2413dbb5e1f3f3f72255))
### Features
* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](https://github.com/sasjs/server/commit/53bf68a6aff44bb7b2f40d40d6554809253a01a8))
## [0.0.77](https://github.com/sasjs/server/compare/v0.0.76...v0.0.77) (2022-05-16)
### Bug Fixes
* **release:** Github workflow without npm token ([c017d13](https://github.com/sasjs/server/commit/c017d13061d21aeacd0690367992d12ca57a115b))
### [0.0.76](https://github.com/sasjs/server/compare/v0.0.75...v0.0.76) (2022-05-16)
### Bug Fixes
* get csrf token from cookie if not present in header ([f89389b](https://github.com/sasjs/server/commit/f89389bbc6f1f8f7060db2bdeb89746cbd60f533))
### [0.0.75](https://github.com/sasjs/server/compare/v0.0.69...v0.0.75) (2022-05-12)
### Features
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
### Bug Fixes
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
* moved getAuthCode from api to web routes ([b40de8f](https://github.com/sasjs/server/commit/b40de8fa6a5aa763ed25a6fe6a381e483e0ab824))
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
* **web:** seperate container for auth code ([5888f04](https://github.com/sasjs/server/commit/5888f04e08a32c6d2c7bcfcbc3a1d32425bff3b3))
### [0.0.74](https://github.com/sasjs/server/compare/v0.0.73...v0.0.74) (2022-05-12)
### Bug Fixes
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
### [0.0.73](https://github.com/sasjs/server/compare/v0.0.72...v0.0.73) (2022-05-10)
### Bug Fixes
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
### [0.0.72](https://github.com/sasjs/server/compare/v0.0.71...v0.0.72) (2022-05-09)
### [0.0.71](https://github.com/sasjs/server/compare/v0.0.70...v0.0.71) (2022-05-07)
### Bug Fixes
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
### [0.0.70](https://github.com/sasjs/server/compare/v0.0.69...v0.0.70) (2022-05-06)
### Features
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
### [0.0.69](https://github.com/sasjs/server/compare/v0.0.68...v0.0.69) (2022-05-02)
### Bug Fixes
* **upload:** appStream uses CSRF + Session authentication ([1f89279](https://github.com/sasjs/server/commit/1f8927926405887f3d134c0a1dd6452ffa33876e))
### [0.0.68](https://github.com/sasjs/server/compare/v0.0.67...v0.0.68) (2022-05-02)
### Bug Fixes
* using monaco editor locally ([2548c82](https://github.com/sasjs/server/commit/2548c82dfe1149e62a570a00546dddd9e30049b1))
### [0.0.67](https://github.com/sasjs/server/compare/v0.0.66...v0.0.67) (2022-05-01)
### [0.0.66](https://github.com/sasjs/server/compare/v0.0.64...v0.0.66) (2022-05-01)
### Bug Fixes
* added swagger ui init file manually ([e2a97fc](https://github.com/sasjs/server/commit/e2a97fcb7c54a57a7ca118677cfce93fe9430d8f))
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
### [0.0.65](https://github.com/sasjs/server/compare/v0.0.64...v0.0.65) (2022-05-01)
### Bug Fixes
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
### [0.0.64](https://github.com/sasjs/server/compare/v0.0.63...v0.0.64) (2022-04-30)
### Bug Fixes
* removed fileExists for serving web ([7b39cc0](https://github.com/sasjs/server/commit/7b39cc06d358f5ffecb87955040c4eb0fcc7469e))
### [0.0.63](https://github.com/sasjs/server/compare/v0.0.62...v0.0.63) (2022-04-30)
### [0.0.62](https://github.com/sasjs/server/compare/v0.0.61...v0.0.62) (2022-04-30)
### [0.0.61](https://github.com/sasjs/server/compare/v0.0.59...v0.0.61) (2022-04-30)
### Bug Fixes
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
### [0.0.60](https://github.com/sasjs/server/compare/v0.0.59...v0.0.60) (2022-04-30)
### Bug Fixes
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
### [0.0.59](https://github.com/sasjs/server/compare/v0.0.58...v0.0.59) (2022-04-29)
### Features
* enabled csrf tokens for web component ([e462aeb](https://github.com/sasjs/server/commit/e462aebdc01f3c0068ed0074473a2063412dcf45))
* enabled session based authentication for web ([5da93f3](https://github.com/sasjs/server/commit/5da93f318aad10b1c67032a467191e4dbb99f411))
### Bug Fixes
* fetch client from DB for each request ([4ad8c81](https://github.com/sasjs/server/commit/4ad8c81e4927c1a82220ec015a781b095c8e859e))
* **web:** show display name instead of username ([e57443f](https://github.com/sasjs/server/commit/e57443f1ed662a022494bb93d79c3d2f10a2d082))
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
### Bug Fixes
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
### Features
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
### Bug Fixes
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
### Bug Fixes
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
### Bug Fixes
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
### Features
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
### Bug Fixes
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
### Features
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
### Features
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
### Features
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
### Bug Fixes
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
### Bug Fixes
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
### Features
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
### Bug Fixes
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
### Bug Fixes
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
### Features
* **drive:** GET folder contents API added ([0ac9e4a](https://github.com/sasjs/server/commit/0ac9e4af7d67c4431053e80eb2384bf5bdc3f8b3))
### [0.0.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
### Bug Fixes
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
### Bug Fixes
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
### Bug Fixes
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
### Bug Fixes
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
### Bug Fixes
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
### Bug Fixes
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
### Bug Fixes
* included sasjs core macros at compile time ([e680901](https://github.com/sasjs/server/commit/e68090181acd844f86f3e81153cb5a4e3f4a307f))
### [0.0.38](https://github.com/sasjs/server/compare/v0.0.37...v0.0.38) (2022-03-23)
### Bug Fixes
* quick fix for executables ([9e53470](https://github.com/sasjs/server/commit/9e53470947350f4b8d835a2cb6b70e3dabf247c4))
### [0.0.37](https://github.com/sasjs/server/compare/v0.0.36...v0.0.37) (2022-03-23)
### Bug Fixes
* appStream html view ([cd00aa2](https://github.com/sasjs/server/commit/cd00aa2af8c7e0df851050a02152dfeddaec7b0f))
* moved macros from codebase to drive ([9ac3191](https://github.com/sasjs/server/commit/9ac3191891bf53ff07135ccec6ddc83b34ea871a))
* **webin:** closes [#99](https://github.com/sasjs/server/issues/99) ([0147bcb](https://github.com/sasjs/server/commit/0147bcb701a209266144147a3746baf1eb1ccc63))
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
### Features
* App Stream, load on startup, new route added ([98a00ec](https://github.com/sasjs/server/commit/98a00ec7ace5da765f049864799be44ba6538e8a))
### Bug Fixes
* **appstream:** app logo + improvements ([df6003d](https://github.com/sasjs/server/commit/df6003df942fd52b956f3d4069d6d7615441d372))
### [0.0.35](https://github.com/sasjs/server/compare/v0.0.33...v0.0.35) (2022-03-21)
### Features
* **cors:** whitelisting is configurable through .env variables ([99f91fb](https://github.com/sasjs/server/commit/99f91fbce2a029dd963ed30c9007a9b046ea6560))
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
### Bug Fixes
* **cors:** removed trailing slashes of urls ([4fd5bf9](https://github.com/sasjs/server/commit/4fd5bf948e4ad8a274d3176d5509163e67980061))
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18)
### Features
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
### Bug Fixes
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
### [0.0.33](https://github.com/sasjs/server/compare/v0.0.32...v0.0.33) (2022-03-16)
### Features
* serve deployed streaming apps ([d6fa877](https://github.com/sasjs/server/commit/d6fa87794155880adc23c2552c37c86ad606c292))
### Bug Fixes
* adde validation + code improvement ([1ff6965](https://github.com/sasjs/server/commit/1ff6965dd2f44ad74136af04b4fba8c76979ecba))
* added api button on web component ([6b708fc](https://github.com/sasjs/server/commit/6b708fcad30d92c21713f9c97bca173c148cc875))
### [0.0.32](https://github.com/sasjs/server/compare/v0.0.31...v0.0.32) (2022-03-14)
### Features
* **web:** added delete option in Drive ([7a6e6c8](https://github.com/sasjs/server/commit/7a6e6c8becab31410d0a36bcc22e13d5359a6cdf))
### [0.0.31](https://github.com/sasjs/server/compare/v0.0.30...v0.0.31) (2022-03-14)
### Features
* **drive:** new route delete file api ([3d583ff](https://github.com/sasjs/server/commit/3d583ff21d344a71aa861c7e5b1426ebc2d54c22))
### Bug Fixes
* added cookie for accessToken ([698180a](https://github.com/sasjs/server/commit/698180ab7e44d67d46c84352ececca5b6c83b230))
* **drive:** update file API is same as create file ([7072e28](https://github.com/sasjs/server/commit/7072e282b1cd1a296d81512c57130237610c1c1e))
* show content of get file api ([6ab42ca](https://github.com/sasjs/server/commit/6ab42ca4868366874f5f21bd711b7b8b72e36774))
* **stp:** return plain/text header for GET & debug ([145ac45](https://github.com/sasjs/server/commit/145ac450365ed39279248ec9321bbe4918bee9fa))
### [0.0.30](https://github.com/sasjs/server/compare/v0.0.29...v0.0.30) (2022-03-05)
### Features
* parse log to array ([c5ad72c](https://github.com/sasjs/server/commit/c5ad72c931ec8fbd7d5a6475838adcbd380c8aee))
* set response headers provded by SAS Code execution ([2c4aa42](https://github.com/sasjs/server/commit/2c4aa420b3119890cafde4265ed5dddbc9d6a636))
### Bug Fixes
* added http headers to /code api as well ([da899b9](https://github.com/sasjs/server/commit/da899b90e26d5ee393eefc302be985eb7c9055a5))
* code api is updated return type ([e2a6810](https://github.com/sasjs/server/commit/e2a6810e9531a8102d3c51fd8df2e1f78f0d965f))
* **file:** fixes response headers ([ef41691](https://github.com/sasjs/server/commit/ef41691e408ef1c1c7a921cc1050bdd533651331))
* get file instead of it's content ([efaf38d](https://github.com/sasjs/server/commit/efaf38d3039391392ce0e14a3accddd8f34ea7d6))
* hot fix for web component ([0a4b202](https://github.com/sasjs/server/commit/0a4b202428e14effc8014a6813cecf7761ce3715))
* improvement in flow of uploading ([8c1941a](https://github.com/sasjs/server/commit/8c1941a87bc184be4e0e09eeff73fc6cb69e3041))
* macros are available Sessions with SASAUTOS ([95843fa](https://github.com/sasjs/server/commit/95843fa4c711aa695ee63ad265b8def4ba56360d))
* minor changes ([0b5f958](https://github.com/sasjs/server/commit/0b5f958f456d291ec7a8697236657c7819d5c654))
* multi-part file upload + validations + specs ([e60f172](https://github.com/sasjs/server/commit/e60f17268d1fa9ab623313026d46bd3f63756f69))
* organized code for usage of multer ([ce0a5e1](https://github.com/sasjs/server/commit/ce0a5e1229bed69c450061fac2bc19711448da56))
* return buffer in case of file response ([3e6234e](https://github.com/sasjs/server/commit/3e6234e6019c5f3ae4280fac079ecc9cb0effc07))
* **stp:** return json for webout ([5005f20](https://github.com/sasjs/server/commit/5005f203b8d6b1d577cdf094b83886bd1fc817a2))
* updating docs ([7312763](https://github.com/sasjs/server/commit/7312763339d6769826328561e2c8d11bbfc0c9f4))
* **upload:** added query param as well for filepath ([feeec4e](https://github.com/sasjs/server/commit/feeec4eb149e9a47e5a52320d1fc95243bf5eb15))
### [0.0.29](https://github.com/sasjs/server/compare/v0.0.28...v0.0.29) (2022-02-16)
### Bug Fixes
* adding .. in folder path ([5931fc1](https://github.com/sasjs/server/commit/5931fc1e712c545ef80454dea5b36e684017c367))
* adding sasjs stpsrv_header() path to autoexec. Relates to [#58](https://github.com/sasjs/server/issues/58) ([ce9bde5](https://github.com/sasjs/server/commit/ce9bde5717369de2d76dc183319be8830b2362b2))
### [0.0.28](https://github.com/sasjs/server/compare/v0.0.27...v0.0.28) (2022-02-16)
### Features
* default macros and bumping core ([6f19d3d](https://github.com/sasjs/server/commit/6f19d3d0ea3815815f246a3e455495c72c8604c7))
### Bug Fixes
* moving core ([f10138b](https://github.com/sasjs/server/commit/f10138b0f2005a958f63cb3a8351e1afa52f086a))
### [0.0.27](https://github.com/sasjs/server/compare/v0.0.26...v0.0.27) (2022-02-16)
### Features
* removing stpsrv_header and updating README with auth details ([d3674c7](https://github.com/sasjs/server/commit/d3674c7f9449d77977e482cd63ccdf7e974fa838))
* **stp-execution:** add returnLog option to execution query ([bf5767e](https://github.com/sasjs/server/commit/bf5767eadfb87f7ed902659347a18361a6a6c74b))
### [0.0.26](https://github.com/sasjs/server/compare/v0.0.25...v0.0.26) (2022-02-14)
### Bug Fixes
* refactored + removed unused package ([d7e1aca](https://github.com/sasjs/server/commit/d7e1aca7e33c3264c784d406fa766e29a6b15ae9))
* release should also has https protocol ([0cfe724](https://github.com/sasjs/server/commit/0cfe724ffa089b84a9f8bca49c9033b56f51c9cb))
* updated token expiry times ([d17a3dd](https://github.com/sasjs/server/commit/d17a3dd5900d5eb88120af8575e3fc7c2cb71ed6))
### [0.0.25](https://github.com/sasjs/server/compare/v0.0.24...v0.0.25) (2022-02-11)
### Bug Fixes
* adding global macvar and bumping sasjs/core with additional server support ([404f1ec](https://github.com/sasjs/server/commit/404f1ec0593a027ed5e84b1d6a84cb9f2d09d99e))
### [0.0.24](https://github.com/sasjs/server/compare/v0.0.23...v0.0.24) (2022-02-11)
### Bug Fixes
* removing sysmacdelete ([480ee4d](https://github.com/sasjs/server/commit/480ee4da831d2a89888c58ebec26bd89802ee2f5))
### [0.0.23](https://github.com/sasjs/server/compare/v0.0.22...v0.0.23) (2022-02-08)
### [0.0.22](https://github.com/sasjs/server/compare/v0.0.17...v0.0.22) (2022-02-08)
### Bug Fixes
* adding missing global vars to autoexec ([1966b17](https://github.com/sasjs/server/commit/1966b17f27e66bf1c9673ef6e1c11f4868b4f816))
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
* bumping core version ([a8df5f4](https://github.com/sasjs/server/commit/a8df5f4afd6c4522270d0a60ab8153dfbdf79e16))
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
### [0.0.21](https://github.com/sasjs/server/compare/v0.0.20...v0.0.21) (2022-02-01)
### Bug Fixes
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
### [0.0.20](https://github.com/sasjs/server/compare/v0.0.2...v0.0.20) (2022-01-20)
### Bug Fixes
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
### [0.0.19](https://github.com/sasjs/server/compare/v0.0.18...v0.0.19) (2022-01-20)
### Bug Fixes
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
### [0.0.18](https://github.com/sasjs/server/compare/v0.0.17...v0.0.18) (2022-01-08)
### Bug Fixes
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
### [0.0.17](https://github.com/sasjs/server/compare/v0.0.16...v0.0.17) (2022-01-07)
### Bug Fixes
* bug removed, log is clean now ([43769e7](https://github.com/sasjs/server/commit/43769e711d37a4f670786545630139a2d926dc76))
### [0.0.16](https://github.com/sasjs/server/compare/v0.0.15...v0.0.16) (2022-01-07)
### Bug Fixes
* added sas9 server address ([cd83891](https://github.com/sasjs/server/commit/cd838915fdb216ee364ea677747409311b1214fb))
* recreate crashed session ([6cbc657](https://github.com/sasjs/server/commit/6cbc657da3eb7fa821a678443a3ae4079c2a1f09))
* session should be marked as consumed ([7a3d710](https://github.com/sasjs/server/commit/7a3d710153f37d12160ff45f8f97fb4fcc75d684))
### [0.0.15](https://github.com/sasjs/server/compare/v0.0.14...v0.0.15) (2022-01-06)
### Bug Fixes
* **studio:** web component updated ([2d77222](https://github.com/sasjs/server/commit/2d77222ae8a139acd9d96466d0e68291c4ebd70e))
* updated route for sas code ([e1eb044](https://github.com/sasjs/server/commit/e1eb04494a5650726c95990f74fc719eced4ccb5))
* **web:** autosave and autofocus ([51ee8c0](https://github.com/sasjs/server/commit/51ee8c0825f021d1d67b2d765d5b434cbf248a1f))
* **web:** parsing of webout ([a115160](https://github.com/sasjs/server/commit/a1151606f21e0007e2b1ca1245d592d96866f62a))
* **web:** sticky tabs on Studio + extra run code button removed ([450d99f](https://github.com/sasjs/server/commit/450d99f06e5929eb1679e6203284e4faa44e19b0))
### [0.0.14](https://github.com/sasjs/server/compare/v0.0.13...v0.0.14) (2021-12-19)
### Bug Fixes
* actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release ([d86c841](https://github.com/sasjs/server/commit/d86c841f1fb94455ac3500f215a42b4acb8b0017))
* bumping sasjs/core with adjustment to ms_webout() ([076b866](https://github.com/sasjs/server/commit/076b866c020fb017512c2764801022a57fe4cca8))
* switch to main branch ([ceca370](https://github.com/sasjs/server/commit/ceca370e2757baf2e8ebb90dab6dfd27f7b990fc))
### [0.0.13](https://github.com/sasjs/server/compare/v0.0.12...v0.0.13) (2021-12-16)
### Features
* **studio:** run selected code + open in studio ([27129a8](https://github.com/sasjs/server/commit/27129a8921084c72968383fdbc2ecbd2f417456c))
### Bug Fixes
* output for Studio ([e5be0e6](https://github.com/sasjs/server/commit/e5be0e678965b05c64bcc8f55c48a366e0ff55a3))
### [0.0.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
### Bug Fixes
* use env if provided for desktop mode ([d19ce25](https://github.com/sasjs/server/commit/d19ce253b4e2d2a7dd912d43a553d4c1bd60ba58))
### [0.0.11](https://github.com/sasjs/server/compare/v0.0.10...v0.0.11) (2021-12-15)
### Features
* added authorization route for web ([#37](https://github.com/sasjs/server/issues/37)) ([d0a1457](https://github.com/sasjs/server/commit/d0a1457f44a3d8993b57106e5e681c4e51fe8e7d))
### [0.0.10](https://github.com/sasjs/server/compare/v0.0.9...v0.0.10) (2021-12-07)
### [0.0.9](https://github.com/sasjs/server/compare/v0.0.3...v0.0.9) (2021-12-07)
### Bug Fixes
* release with files ([#35](https://github.com/sasjs/server/issues/35)) ([a0822e6](https://github.com/sasjs/server/commit/a0822e6b61905257475121ffd907fd1f79ed146b))
### [0.0.8](https://github.com/saadjutt01/server/compare/v0.0.7...v0.0.8) (2021-12-07)
### [0.0.7](https://github.com/saadjutt01/server/compare/v0.0.6...v0.0.7) (2021-12-07)
### [0.0.6](https://github.com/saadjutt01/server/compare/v0.0.5...v0.0.6) (2021-12-07)
### [0.0.5](https://github.com/saadjutt01/server/compare/v0.0.4...v0.0.5) (2021-12-07)
### 0.0.4 (2021-12-07)
### Features
* add api endpoint for sasjs drive ([96b5fef](https://github.com/saadjutt01/server/commit/96b5fef3021f67f66e5e3b854319230618421852))
* add new type TreeNode ([bc3cb7b](https://github.com/saadjutt01/server/commit/bc3cb7bb20a1202d17aaf8bbcddd1feef4fff724))
* add pug and directory tree dependencies ([3ffa168](https://github.com/saadjutt01/server/commit/3ffa168c8bafc989caf1a744cebc20d36c6aa11b))
* add sasjsExecutor controller ([279fbf2](https://github.com/saadjutt01/server/commit/279fbf2a9a0bd6bc0938f9a66e9685fb93d86089))
* add top app bar with tab navigation ([a506bc9](https://github.com/saadjutt01/server/commit/a506bc9dd9d201b89fc9ffd1a552c16bd170f058))
* add views and styles for rendering html ([a446f5c](https://github.com/saadjutt01/server/commit/a446f5c4f73a4e829a2c5eec041e3adffeddff52))
* adding _metaperson and _metauser to Stored Programs ([b3147ec](https://github.com/saadjutt01/server/commit/b3147ec680646b3d9c7e89152e472dddc8a36075))
* **api-utility:** create getWebBuildFolderPath utility ([9648c51](https://github.com/saadjutt01/server/commit/9648c51b5491d8b6bbe5497273efa2d11e2486d2))
* **api:** set up endpoint for sas code execution ([f6046b1](https://github.com/saadjutt01/server/commit/f6046b15ae30cd8ace685cf283339871de658b7d))
* authentication with jwt ([22dfcfd](https://github.com/saadjutt01/server/commit/22dfcfddb9abd355a63d1ee5acd925c759e86d69))
* compile systemInit and inject to autoExec ([b75139d](https://github.com/saadjutt01/server/commit/b75139dda5cacc7e10a4d635eb2a222f7dfa3fec))
* **deploy:** add appLoc ([f0f1e1d](https://github.com/saadjutt01/server/commit/f0f1e1d57ea1e961fc3b1cfcbd4cb259a77a90d0))
* **deploy:** add route to deploy a file tree to @sasjs/server ([b4bf72f](https://github.com/saadjutt01/server/commit/b4bf72f70401a81b6d5d0104332a1fbc5f71562b))
* **execute:** add macroVars to job execution ([39e486b](https://github.com/saadjutt01/server/commit/39e486b8cb5efbadc86eb7029b60c7073744eb2b))
* **execute:** add sas controller ([bf1db4d](https://github.com/saadjutt01/server/commit/bf1db4dd47d2488bac073cd468db920ff9fd533d))
* **execution:** add ExecutionController working with session ([8b25641](https://github.com/saadjutt01/server/commit/8b2564120def137f80647064e28062b880d58efe))
* **executor:** improved api response ([707b503](https://github.com/saadjutt01/server/commit/707b50394267217e717aa72f74dbeba3852a93e6))
* **executor:** response with webout ([52275ba](https://github.com/saadjutt01/server/commit/52275ba67d97d5cbdf6c5511c9bd789bd6ca6b4e))
* **express:** increase payload max size ([7b403c1](https://github.com/saadjutt01/server/commit/7b403c151e889cae975944546bb4bb53eff1dd26))
* frontend app for sasjs server ([db8eb8d](https://github.com/saadjutt01/server/commit/db8eb8dd7197bbe36f2d10cabbb58b3eb7ce7c33))
* generate executables for sasjs/server with web component ([514a262](https://github.com/saadjutt01/server/commit/514a262340dc34007de75caf08ad03969e7110c1))
* Groups are added + docs ([2fe9d5c](https://github.com/saadjutt01/server/commit/2fe9d5ca9ce1fb376f03534f8685d65efb2f68a6))
* improved deploy and execute endpoints ([5b4e562](https://github.com/saadjutt01/server/commit/5b4e5626fc7ae3e020819e3ebd334cc3712ae8e7))
* JWT saved in DB + logout api added ([46c5a75](https://github.com/saadjutt01/server/commit/46c5a75ac4fb26ebec219118eb204f1b5049ae90))
* **routes:** separate routes into web and api ([dabef59](https://github.com/saadjutt01/server/commit/dabef597287a59f3bfaff54a18de465f820aa514))
* **session:** add SessionController ([6a34fa1](https://github.com/saadjutt01/server/commit/6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9))
* **session:** add SessionController and ExecutionController ([6e0b04a](https://github.com/saadjutt01/server/commit/6e0b04a6e548ac31baee726c9249b7e25f50f0bf))
* user operation apis added ([728f277](https://github.com/saadjutt01/server/commit/728f277f5ce136d62951071833cd6db478b07e4a))
### Bug Fixes
* **api-cdrive-oller:** throw erow error when file not found ([03d1d60](https://github.com/saadjutt01/server/commit/03d1d60660fc46421ef6ad9cee8493dd884e309a))
* change api endpoint SASjsExecutor/do -> SASjsApi/stp/execute ([d93673f](https://github.com/saadjutt01/server/commit/d93673f2a51098c6af8abc4a793081d4591e27de))
* cors enabled for desktop mode ([2bb10c7](https://github.com/saadjutt01/server/commit/2bb10c71661b5de7ed515c82e5b1967b88449972))
* DB names updates + refresh api is added ([9f17b17](https://github.com/saadjutt01/server/commit/9f17b17e3138ce49f24447cd5ae457e3e90ad4da))
* debug not passed ([d9555e1](https://github.com/saadjutt01/server/commit/d9555e151b0e1d1a4068efdf8ee9ed53b25b9b89))
* **deploy:** fix payload processing ([361b539](https://github.com/saadjutt01/server/commit/361b539271cf95bbe570cca9e44635ab563d3f9e))
* **deps:** removed malicious dependency ([c4b9402](https://github.com/saadjutt01/server/commit/c4b9402f017b76dc412a17a10313f1fd5a3891ef))
* **docker:** docker-compose for prod+development ([4a363c5](https://github.com/saadjutt01/server/commit/4a363c5b9796283199debcc8afa810c6f561f8e6))
* **executor:** create tmp files before execution ([cdbc3fd](https://github.com/saadjutt01/server/commit/cdbc3fd298e2a581773448bdddcad93de3b3544d))
* **executor:** fix nosplash argument and api response ([715b1de](https://github.com/saadjutt01/server/commit/715b1dec68377eefe03aa8203a73debe77842436))
* fix web route ([6c7a6b6](https://github.com/saadjutt01/server/commit/6c7a6b6c6af28c29b391162e4e332da6524b1c61))
* **github:** fixed github flow ([8dab288](https://github.com/saadjutt01/server/commit/8dab28861dfa7c4c7fefc7fe038df50f58d04547))
* **github:** removed npm token ([bbb94d6](https://github.com/saadjutt01/server/commit/bbb94d61ce39c84a6c0c44186e89787ab0e76a8c))
* immplementation of files api fixed ([299319e](https://github.com/saadjutt01/server/commit/299319e2dbe06c7ca99e403fcbdec2ad1db8b7e4))
* load file when url contains filePath ([99d5577](https://github.com/saadjutt01/server/commit/99d55775aaac3b2caaa4b10d4ed698f6cd7fcb2a))
* modify the directory tree algorithm to include relative path with each node ([91e2e2b](https://github.com/saadjutt01/server/commit/91e2e2bc4a46da0d149578593559efdb87681bd4))
* norefferer issue in home page external links fix ([e2b12b7](https://github.com/saadjutt01/server/commit/e2b12b74f52c3ce4541fde9af6af0093b56b157b))
* on clicking execute button open new tab for response ([02f5371](https://github.com/saadjutt01/server/commit/02f5371f57b311ff700ba8108f9d5168da8c22a4))
* prettier ([716ae81](https://github.com/saadjutt01/server/commit/716ae81d9293b42dd2a7047ac52d75401b3b8798))
* **prod-server:** use port from configuration ([4d8efbb](https://github.com/saadjutt01/server/commit/4d8efbb88d32154d84e80b79780e2e3de2f519e4))
* readme overview| ([b3342f0](https://github.com/saadjutt01/server/commit/b3342f00031d19080fb72e3460f023c5f44bac95))
* remove .sas extension from _program parameter at the end of string ([56cb2d1](https://github.com/saadjutt01/server/commit/56cb2d1d512beadb5cfdc4ab4034ac917311ff23))
* removing renegade dash ([4ff4d39](https://github.com/saadjutt01/server/commit/4ff4d39e954e895b46ddc3e2919f7f2c4e1ce01d))
* **root-package.json:** lint:fix command fixed in root package json ([ec6333f](https://github.com/saadjutt01/server/commit/ec6333f6aa67c1b94f54b017ed27eb3b21b4207f))
* **routes:** fix routes imports ([49c152a](https://github.com/saadjutt01/server/commit/49c152a398b60f6b0a0c25a68eb4c1c291984872))
* **semantic-release:** fixed package.json ([ef45787](https://github.com/saadjutt01/server/commit/ef45787019f1e61d0e4e2acee334236e8aca23cc))
* sending _webout as result object in response JSON ([b97523e](https://github.com/saadjutt01/server/commit/b97523e55584cc7d9d682cfeaab8f5b70a10b899))
* session refactoring with Saad & Allan ([cbe07b4](https://github.com/saadjutt01/server/commit/cbe07b4abb2e936037874af1a088cd038e0fc731))
* **ts:** enable files ([37b6936](https://github.com/saadjutt01/server/commit/37b6936cca3cff9c1ca26ec7b4b938a357c448df))
* update api calls from client side ([031e492](https://github.com/saadjutt01/server/commit/031e492d44674dec4f2b3bc1f5bf7affac5716bd))
* update api endpoints ([936a205](https://github.com/saadjutt01/server/commit/936a205e66073b9178089c6ab10d6ac3bf323c54))
* update sasjs drive controller from function base to class base ([3fe475d](https://github.com/saadjutt01/server/commit/3fe475d477c466556659b48c70eeac5153ff5b0e))
* update SASjsApi/stp/execute post api endpoints to capture url params ([d981444](https://github.com/saadjutt01/server/commit/d9814441bb1d269ec2404e50f51124f998c65c40))
* use hash router instead of browser router in react app ([c72867d](https://github.com/saadjutt01/server/commit/c72867d5a70550660c8c37220aa33693716a93f1))
* **web:** infinite call to api end point fixed ([ac745c8](https://github.com/saadjutt01/server/commit/ac745c8f5c3e4aa2ac8d6ca23bb1276452d4018b))
* **web:** remove unnecessary packages and files ([0fb4301](https://github.com/saadjutt01/server/commit/0fb43019668f5a13f6e77fdb4b3e543006b509c0))
* **weeb:** add catch block with each axios request ([552a358](https://github.com/saadjutt01/server/commit/552a3584ec9345bc1dec0ff5377bf773a7928d62))
* **workflow:** fix 'SASjs Server Build' ([174d94a](https://github.com/saadjutt01/server/commit/174d94a23c5036d61a4f2e11296283f128d4dafa))
### 0.0.3 (2021-11-30)
### Features
* add api endpoint for sasjs drive ([96b5fef](https://github.com/sasjs/server/commit/96b5fef3021f67f66e5e3b854319230618421852))
* add new type TreeNode ([bc3cb7b](https://github.com/sasjs/server/commit/bc3cb7bb20a1202d17aaf8bbcddd1feef4fff724))
* add pug and directory tree dependencies ([3ffa168](https://github.com/sasjs/server/commit/3ffa168c8bafc989caf1a744cebc20d36c6aa11b))
* add sasjsExecutor controller ([279fbf2](https://github.com/sasjs/server/commit/279fbf2a9a0bd6bc0938f9a66e9685fb93d86089))
* add top app bar with tab navigation ([a506bc9](https://github.com/sasjs/server/commit/a506bc9dd9d201b89fc9ffd1a552c16bd170f058))
* add views and styles for rendering html ([a446f5c](https://github.com/sasjs/server/commit/a446f5c4f73a4e829a2c5eec041e3adffeddff52))
* adding _metaperson and _metauser to Stored Programs ([b3147ec](https://github.com/sasjs/server/commit/b3147ec680646b3d9c7e89152e472dddc8a36075))
* **api-utility:** create getWebBuildFolderPath utility ([9648c51](https://github.com/sasjs/server/commit/9648c51b5491d8b6bbe5497273efa2d11e2486d2))
* **api:** set up endpoint for sas code execution ([f6046b1](https://github.com/sasjs/server/commit/f6046b15ae30cd8ace685cf283339871de658b7d))
* authentication with jwt ([22dfcfd](https://github.com/sasjs/server/commit/22dfcfddb9abd355a63d1ee5acd925c759e86d69))
* compile systemInit and inject to autoExec ([b75139d](https://github.com/sasjs/server/commit/b75139dda5cacc7e10a4d635eb2a222f7dfa3fec))
* **deploy:** add appLoc ([f0f1e1d](https://github.com/sasjs/server/commit/f0f1e1d57ea1e961fc3b1cfcbd4cb259a77a90d0))
* **deploy:** add route to deploy a file tree to @sasjs/server ([b4bf72f](https://github.com/sasjs/server/commit/b4bf72f70401a81b6d5d0104332a1fbc5f71562b))
* **execute:** add macroVars to job execution ([39e486b](https://github.com/sasjs/server/commit/39e486b8cb5efbadc86eb7029b60c7073744eb2b))
* **execute:** add sas controller ([bf1db4d](https://github.com/sasjs/server/commit/bf1db4dd47d2488bac073cd468db920ff9fd533d))
* **execution:** add ExecutionController working with session ([8b25641](https://github.com/sasjs/server/commit/8b2564120def137f80647064e28062b880d58efe))
* **executor:** improved api response ([707b503](https://github.com/sasjs/server/commit/707b50394267217e717aa72f74dbeba3852a93e6))
* **executor:** response with webout ([52275ba](https://github.com/sasjs/server/commit/52275ba67d97d5cbdf6c5511c9bd789bd6ca6b4e))
* **express:** increase payload max size ([7b403c1](https://github.com/sasjs/server/commit/7b403c151e889cae975944546bb4bb53eff1dd26))
* frontend app for sasjs server ([db8eb8d](https://github.com/sasjs/server/commit/db8eb8dd7197bbe36f2d10cabbb58b3eb7ce7c33))
* generate executables for sasjs/server with web component ([514a262](https://github.com/sasjs/server/commit/514a262340dc34007de75caf08ad03969e7110c1))
* Groups are added + docs ([2fe9d5c](https://github.com/sasjs/server/commit/2fe9d5ca9ce1fb376f03534f8685d65efb2f68a6))
* improved deploy and execute endpoints ([5b4e562](https://github.com/sasjs/server/commit/5b4e5626fc7ae3e020819e3ebd334cc3712ae8e7))
* JWT saved in DB + logout api added ([46c5a75](https://github.com/sasjs/server/commit/46c5a75ac4fb26ebec219118eb204f1b5049ae90))
* **routes:** separate routes into web and api ([dabef59](https://github.com/sasjs/server/commit/dabef597287a59f3bfaff54a18de465f820aa514))
* **session:** add SessionController ([6a34fa1](https://github.com/sasjs/server/commit/6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9))
* **session:** add SessionController and ExecutionController ([6e0b04a](https://github.com/sasjs/server/commit/6e0b04a6e548ac31baee726c9249b7e25f50f0bf))
* user operation apis added ([728f277](https://github.com/sasjs/server/commit/728f277f5ce136d62951071833cd6db478b07e4a))
### Bug Fixes
* **api-cdrive-oller:** throw erow error when file not found ([03d1d60](https://github.com/sasjs/server/commit/03d1d60660fc46421ef6ad9cee8493dd884e309a))
* change api endpoint SASjsExecutor/do -> SASjsApi/stp/execute ([d93673f](https://github.com/sasjs/server/commit/d93673f2a51098c6af8abc4a793081d4591e27de))
* cors enabled for desktop mode ([2bb10c7](https://github.com/sasjs/server/commit/2bb10c71661b5de7ed515c82e5b1967b88449972))
* DB names updates + refresh api is added ([9f17b17](https://github.com/sasjs/server/commit/9f17b17e3138ce49f24447cd5ae457e3e90ad4da))
* debug not passed ([d9555e1](https://github.com/sasjs/server/commit/d9555e151b0e1d1a4068efdf8ee9ed53b25b9b89))
* **deploy:** fix payload processing ([361b539](https://github.com/sasjs/server/commit/361b539271cf95bbe570cca9e44635ab563d3f9e))
* **deps:** removed malicious dependency ([c4b9402](https://github.com/sasjs/server/commit/c4b9402f017b76dc412a17a10313f1fd5a3891ef))
* **docker:** docker-compose for prod+development ([4a363c5](https://github.com/sasjs/server/commit/4a363c5b9796283199debcc8afa810c6f561f8e6))
* **executor:** create tmp files before execution ([cdbc3fd](https://github.com/sasjs/server/commit/cdbc3fd298e2a581773448bdddcad93de3b3544d))
* **executor:** fix nosplash argument and api response ([715b1de](https://github.com/sasjs/server/commit/715b1dec68377eefe03aa8203a73debe77842436))
* fix web route ([6c7a6b6](https://github.com/sasjs/server/commit/6c7a6b6c6af28c29b391162e4e332da6524b1c61))
* **github:** fixed github flow ([8dab288](https://github.com/sasjs/server/commit/8dab28861dfa7c4c7fefc7fe038df50f58d04547))
* **github:** removed npm token ([bbb94d6](https://github.com/sasjs/server/commit/bbb94d61ce39c84a6c0c44186e89787ab0e76a8c))
* immplementation of files api fixed ([299319e](https://github.com/sasjs/server/commit/299319e2dbe06c7ca99e403fcbdec2ad1db8b7e4))
* load file when url contains filePath ([99d5577](https://github.com/sasjs/server/commit/99d55775aaac3b2caaa4b10d4ed698f6cd7fcb2a))
* modify the directory tree algorithm to include relative path with each node ([91e2e2b](https://github.com/sasjs/server/commit/91e2e2bc4a46da0d149578593559efdb87681bd4))
* norefferer issue in home page external links fix ([e2b12b7](https://github.com/sasjs/server/commit/e2b12b74f52c3ce4541fde9af6af0093b56b157b))
* on clicking execute button open new tab for response ([02f5371](https://github.com/sasjs/server/commit/02f5371f57b311ff700ba8108f9d5168da8c22a4))
* prettier ([716ae81](https://github.com/sasjs/server/commit/716ae81d9293b42dd2a7047ac52d75401b3b8798))
* **prod-server:** use port from configuration ([4d8efbb](https://github.com/sasjs/server/commit/4d8efbb88d32154d84e80b79780e2e3de2f519e4))
* readme overview| ([b3342f0](https://github.com/sasjs/server/commit/b3342f00031d19080fb72e3460f023c5f44bac95))
* remove .sas extension from _program parameter at the end of string ([56cb2d1](https://github.com/sasjs/server/commit/56cb2d1d512beadb5cfdc4ab4034ac917311ff23))
* removing renegade dash ([4ff4d39](https://github.com/sasjs/server/commit/4ff4d39e954e895b46ddc3e2919f7f2c4e1ce01d))
* **root-package.json:** lint:fix command fixed in root package json ([ec6333f](https://github.com/sasjs/server/commit/ec6333f6aa67c1b94f54b017ed27eb3b21b4207f))
* **routes:** fix routes imports ([49c152a](https://github.com/sasjs/server/commit/49c152a398b60f6b0a0c25a68eb4c1c291984872))
* **semantic-release:** fixed package.json ([ef45787](https://github.com/sasjs/server/commit/ef45787019f1e61d0e4e2acee334236e8aca23cc))
* sending _webout as result object in response JSON ([b97523e](https://github.com/sasjs/server/commit/b97523e55584cc7d9d682cfeaab8f5b70a10b899))
* session refactoring with Saad & Allan ([cbe07b4](https://github.com/sasjs/server/commit/cbe07b4abb2e936037874af1a088cd038e0fc731))
* **ts:** enable files ([37b6936](https://github.com/sasjs/server/commit/37b6936cca3cff9c1ca26ec7b4b938a357c448df))
* update api calls from client side ([031e492](https://github.com/sasjs/server/commit/031e492d44674dec4f2b3bc1f5bf7affac5716bd))
* update api endpoints ([936a205](https://github.com/sasjs/server/commit/936a205e66073b9178089c6ab10d6ac3bf323c54))
* update sasjs drive controller from function base to class base ([3fe475d](https://github.com/sasjs/server/commit/3fe475d477c466556659b48c70eeac5153ff5b0e))
* update SASjsApi/stp/execute post api endpoints to capture url params ([d981444](https://github.com/sasjs/server/commit/d9814441bb1d269ec2404e50f51124f998c65c40))
* use hash router instead of browser router in react app ([c72867d](https://github.com/sasjs/server/commit/c72867d5a70550660c8c37220aa33693716a93f1))
* **web:** infinite call to api end point fixed ([ac745c8](https://github.com/sasjs/server/commit/ac745c8f5c3e4aa2ac8d6ca23bb1276452d4018b))
* **web:** remove unnecessary packages and files ([0fb4301](https://github.com/sasjs/server/commit/0fb43019668f5a13f6e77fdb4b3e543006b509c0))
* **weeb:** add catch block with each axios request ([552a358](https://github.com/sasjs/server/commit/552a3584ec9345bc1dec0ff5377bf773a7928d62))
* **workflow:** fix 'SASjs Server Build' ([174d94a](https://github.com/sasjs/server/commit/174d94a23c5036d61a4f2e11296283f128d4dafa))

View File

@@ -1,5 +1,4 @@
FROM node:lts-alpine
RUN npm install -g @sasjs/cli
WORKDIR /usr/server/api
COPY ["package.json","package-lock.json", "./"]
RUN npm ci

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 SASjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

259
README.md
View File

@@ -1,117 +1,230 @@
# SASjs Server
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or it could even run locally on your desktop. It provides the following functionality:
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
- Virtual filesystem for storing SAS programs and other content
- Ability to execute Stored Programs from a URL
- Ability to create web apps using simple Desktop SAS
- REST API with Swagger Docs
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
One major benefit of using SASjs Server alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library, is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
## Configuration
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentication, and a database)
Configuration is made in the `configuration` section of `package.json`:
## Installation
- Provide path to SAS9 executable.
Installation can be made programmatically using command line, or by manually downloading and running the executable.
### Using dockers:
### Programmatic
There is `.env.example` file present at root of the project. [for Production]
Fetch the relevant package from github using `curl`, eg as follows (for linux):
There is `.env.example` file present at `./api` of the project. [for Development]
There is `.env.example` file present at `./web` of the project. [for Development]
Remember to provide enviornment variables.
#### Development
Command to run docker for development:
```
docker-compose up -d
```bash
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
```
It uses default docker compose file i.e. `docker-compose.yml` present at root.
It will build following images if running first time:
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
### Manual
#### Production
1. Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
2. Trigger by double clicking (windows) or executing from commandline.
Command to run docker for production:
You are presented with two prompts (if not set as ENV vars):
- Location of your `sas.exe` / `sas.sh` executable
- Path to a filesystem location for Stored Programs and temporary files
## ENV Var configuration
When launching the app, it will make use of specific environment variables. These can be set in the following places:
- Configured globally in `/etc/environment` file
- Export in terminal or shell script (`export VAR=VALUE`)
- Prepended in the command
- Enter in the `.env` file alongside the executable
Example contents of a `.env` file:
```
docker-compose -f docker-compose.prod.yml up -d
```
#
## Core Settings
#
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
It will build following images if running first time:
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
# MODE options: [desktop|server] default: `desktop`
# Desktop mode is single user and designed for workstation use
# Server mode is multi-user and suitable for intranet / internet use
MODE=
### Using node:
# Path to SAS executable (sas.exe / sas.sh)
SAS_PATH=/path/to/sas/executable.exe
#### Development (running api and web seperately):
# Path to Node.js executable
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
##### API
# Path to working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root
Navigate to `./api`
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
# options: [http|https] default: http
PROTOCOL=
# default: 5000
PORT=
#
## Additional SAS Options
#
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
# Any options set here are automatically applied in the SAS session
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
SAS_OPTIONS= -NOXCMD
SASV9_OPTIONS= -NOXCMD
#
## Additional Web Server Options
#
# ENV variables required for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem
# ENV variables required for MODE: `server`
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
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
# options: <http://localhost:3000 https://abc.com ...> space separated urls
WHITELIST=
# HELMET Cross Origin Embedder Policy
# 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=
# HELMET Content Security Policy
# Path to a json file containing HELMET `contentSecurityPolicy` directives
# Docs: https://helmetjs.github.io/#reference
#
# Example config:
# {
# "img-src": ["'self'", "data:"],
# "script-src": ["'self'", "'unsafe-inline'"],
# "script-src-attr": ["'self'", "'unsafe-inline'"]
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
# 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 cSAS_PATHomes first in string.
# Possible options at the moment are sas and js
# options: [sas,js|js,sas|sas|js] default:sas,js
RUN_TIMES=
```
npm install
npm start
## Persisting the Session
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
1. Linux Background Job
2. NPM package `pm2`
### Background Job
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
### PM2
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
```bash
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
export PORT=5001
export SASJS_ROOT=./sasjs_root
pm2 start api-linux
```
##### Web
To get the logs (and some useful commands):
Navigate to `./web`
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
npm install
npm start
```bash
pm2 [list|ls|status]
pm2 logs
pm2 logs --lines 200
```
#### Development (running only api server and have web build served):
##### API server also serving Web build files
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
Managing processes:
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm start
pm2 restart app_name
pm2 reload app_name
pm2 stop app_name
pm2 delete app_name
```
#### Production
Instead of `app_name` you can pass:
##### API & WEB
- `all` to act on all processes
- `id` to act on a specific process id
```
npm run server
```
## Server Version
This will install/build `web` and install `api`, then start prod server.
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
## Executables
- CLIENTID: `clientID1`
- USERNAME: `secretuser`
- PASSWORD: `secretpassword`
Command to generate executables
## Contributors ✨
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm run exe
```
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
This will install/build web app and install/create executables of sasjs server at root `./executables`
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Saad Jutt</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=sabhas" title="Tests">⚠️</a></td>
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Tests">⚠️</a></td>
<td align="center"><a href="https://4gl.io/"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vladislav Parhomchik</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=VladislavParhomchik" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kknapen"><img src="https://avatars.githubusercontent.com/u/78609432?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Koen Knapen</b></sub></a><br /><a href="#userTesting-kknapen" title="User Testing">📓</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

89
SASjsServer.drawio Normal file
View File

@@ -0,0 +1,89 @@
<mxfile host="65bd71144e">
<diagram id="HJy_QFGaI9JSrArARLup" name="Page-1">
<mxGraphModel dx="1908" dy="2140" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="4" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
<mxGeometry x="-360" y="-120" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="7" value="SASjs Server" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;fontSize=30;" vertex="1" parent="1">
<mxGeometry x="30" y="-150" width="360" height="850" as="geometry"/>
</mxCell>
<mxCell id="8" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="23" as="sourcePoint"/>
<mxPoint x="115" y="22.586363636363558" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="11" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;span style=&quot;color: #a31515&quot;&gt;/SASjsApi/auth/authorize&lt;br&gt;(username,password,clientId)&lt;/span&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="14" value="" style="edgeStyle=none;html=1;exitX=-0.002;exitY=0.874;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="110" y="80" as="sourcePoint"/>
<mxPoint x="-340" y="80" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="16" value="&lt;font color=&quot;#a31515&quot; face=&quot;menlo, monaco, courier new, monospace&quot;&gt;&lt;span style=&quot;font-size: 12px&quot;&gt;`code`&lt;/span&gt;&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="14">
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="21" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
<mxGeometry x="-360" y="545" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="22" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="30">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="165" as="sourcePoint"/>
<mxPoint x="115" y="165" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="23" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; line-height: 18px&quot;&gt;&lt;span style=&quot;color: #a31515&quot;&gt;/SASjsApi/auth/token&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;color: #a31515&quot;&gt;(clientId,code)&lt;/span&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="22">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="edgeStyle=none;html=1;exitX=0.009;exitY=0.905;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="30">
<mxGeometry relative="1" as="geometry">
<mxPoint x="210" y="222.5" as="sourcePoint"/>
<mxPoint x="-340" y="223" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="&lt;font color=&quot;#a31515&quot; face=&quot;menlo, monaco, courier new, monospace&quot;&gt;&lt;span style=&quot;font-size: 12px&quot;&gt;`&lt;/span&gt;&lt;/font&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;accessToken&lt;/span&gt;&lt;span style=&quot;font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace&quot;&gt;` &amp;amp; `&lt;/span&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;refreshToken&lt;/span&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;`&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="24">
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;" edge="1" parent="1" source="21" target="4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="40" y="240" as="sourcePoint"/>
<mxPoint x="90" y="190" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="28" value="&lt;span&gt;Validates&lt;/span&gt;&lt;br&gt;&lt;span&gt;username/password/clientId&lt;/span&gt;&lt;br&gt;&lt;span&gt;and issue short&lt;/span&gt;&lt;br&gt;&lt;span&gt;Authorization code&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="115" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="30" value="Validates&lt;br&gt;clientId &amp;amp; authorization code&lt;br&gt;and issue&lt;br&gt;Access Token &amp;amp; Refresh Token" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="115" y="140" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="32" value="Protected APIs&lt;br&gt;Authenticate requests &lt;br&gt;with provided Bearer Token" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="50" y="280" width="320" height="400" as="geometry"/>
</mxCell>
<mxCell id="33" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.373;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="32">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="432.5" as="sourcePoint"/>
<mxPoint x="-10" y="430" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="34" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; line-height: 18px&quot;&gt;&lt;font color=&quot;#a31515&quot;&gt;Request with Access Token&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,8 +1,26 @@
MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
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
PORT=[5000] default value is 5000
PORT_WEB=[port for sasjs web component(react)] default value is 3000
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>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas,js
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
SASJS_ROOT=./sasjs_root
LOG_FORMAT_MORGAN=common

1
api/.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16.15.1

13
api/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": ["run-script", "start"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
}
]
}

View File

@@ -0,0 +1,5 @@
{
"img-src": ["'self'", "data:"],
"script-src": ["'self'", "'unsafe-inline'"],
"script-src-attr": ["'self'", "'unsafe-inline'"]
}

10269
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,36 @@
{
"name": "api",
"version": "0.0.1",
"version": "0.0.2",
"description": "Api of SASjs server",
"main": "./src/server.ts",
"scripts": {
"initial": "npm run swagger && npm run compileSysInit",
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
"prestart": "npm run initial",
"prestart:prod": "npm run initial",
"prebuild": "npm run initial",
"start": "nodemon ./src/server.ts",
"start:prod": "nodemon ./src/prod-server.ts",
"start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc",
"postbuild": "npm run copy:files",
"swagger": "tsoa spec",
"semantic-release": "semantic-release -d",
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
"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}\"",
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
"exe": "npm run build && npm run exe:copy && pkg .",
"exe:copy": "npm run public:copy && npm run sasjsbuild:copy && npm run web:copy",
"exe": "npm run build && pkg .",
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore: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/",
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
"compileSysInit": "ts-node ./scripts/compileSysInit.ts"
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
},
"bin": "./build/src/server.js",
"pkg": {
"assets": [
"./build/public/**/*",
"./build/sasjsbuild/**/*",
"./build/sasjscore/**/*",
"./web/build/**/*"
],
"targets": [
@@ -41,29 +42,39 @@
},
"release": {
"branches": [
"master"
"main"
]
},
"author": "Analytium Ltd",
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^2.48.6",
"@sasjs/utils": "2.34.1",
"@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-session": "^1.17.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",
"swagger-ui-express": "^4.1.6",
"tsoa": "^3.14.0"
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"url": "^0.10.3"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
"@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/jsonwebtoken": "^8.5.5",
"@types/mongoose-sequence": "^3.0.6",
@@ -72,20 +83,25 @@
"@types/node": "^15.12.2",
"@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",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7",
"pkg": "^5.4.1",
"pkg": "5.6.0",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"semantic-release": "^17.4.3",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",
"tsoa": "3.14.1",
"typescript": "^4.3.2"
},
"configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4"
"nodemonConfig": {
"ignore": [
"sasjs_root/**/*"
]
}
}

View File

@@ -0,0 +1,50 @@
window.onload = function () {
// Build a system
var url = window.location.search.match(/url=([^&]+)/)
if (url && url.length > 1) {
url = decodeURIComponent(url[1])
} else {
url = window.location.origin
}
var options = {
customOptions: {
url: '/swagger.yaml',
requestInterceptor: function (request) {
request.credentials = 'include'
var cookie = document.cookie
var startIndex = cookie.indexOf('XSRF-TOKEN')
var csrf = cookie.slice(startIndex + 11).split('; ')[0]
request.headers['X-XSRF-TOKEN'] = csrf
return request
}
}
}
url = options.swaggerUrl || url
var urls = options.swaggerUrls
var customOptions = options.customOptions
var spec1 = options.swaggerDoc
var swaggerOptions = {
spec: spec1,
url: url,
urls: urls,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: 'StandaloneLayout'
}
for (var attrname in customOptions) {
swaggerOptions[attrname] = customOptions[attrname]
}
var ui = SwaggerUIBundle(swaggerOptions)
if (customOptions.oauth) {
ui.initOAuth(customOptions.oauth)
}
if (customOptions.authAction) {
ui.authActions.authorize(customOptions.authAction)
}
window.ui = ui
}

View File

@@ -0,0 +1,49 @@
const inputElement = document.getElementById('fileId')
document.getElementById('uploadButton').addEventListener('click', function () {
inputElement.click()
})
inputElement.addEventListener(
'change',
function () {
const fileList = this.files /* now you can work with the file list */
updateFileUploadMessage('Requesting ...')
const file = fileList[0]
const formData = new FormData()
formData.append('file', file)
axios
.post('/SASjsApi/drive/deploy/upload', formData)
.then((res) => res.data)
.then((data) => {
return (
data.message +
'\nstreamServiceName: ' +
data.streamServiceName +
'\nrefreshing page once alert box closes.'
)
})
.then((message) => {
alert(message)
location.reload()
})
.catch((error) => {
alert(error.response.data)
resetFileUpload()
updateFileUploadMessage('Upload New App')
})
},
false
)
function updateFileUploadMessage(message) {
document.getElementById('uploadMessage').innerHTML = message
}
function resetFileUpload() {
inputElement.value = null
}

3
api/public/axios.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
api/public/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

21
api/public/sasjs-logo.svg Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F6E40C;}
</style>
<rect id="XMLID_1_" width="32" height="32"/>
<g id="XMLID_654_">
<path id="XMLID_656_" class="st0" d="M27.9,17.4c-1.1,0-2.1,0-3,0c-1.2,0-2.3,0-3.5,0c-0.5,0-0.7,0.2-0.6,0.7c0,2.1,0,4.3,0,6.4
c0,0.5-0.2,0.8-0.6,1c-2.5,1.4-4.9,2.8-7.3,4.3c-0.4,0.2-0.6,0.2-1,0c-2.4-1.4-4.9-2.9-7.3-4.3c-0.2-0.1-0.5-0.5-0.5-0.7
c0-3.2,0-6.4,0-9.6c0-0.1,0-0.1,0.1-0.3c0.3,0,0.5,0,0.8,0c1.9,0,3.7,0,5.6,0c0.6,0,0.7-0.2,0.7-0.7c0-2.1,0-4.2,0-6.4
c0-0.5,0.1-0.8,0.6-1.1c2.5-1.4,4.9-2.9,7.3-4.3c0.2-0.1,0.6-0.1,0.9,0c2.5,1.4,5,2.9,7.5,4.4c0.2,0.1,0.4,0.4,0.4,0.6
C27.9,10.6,27.9,13.9,27.9,17.4z M20.8,14.8c1.4,0,2.7,0,4,0c0.5,0,0.7-0.2,0.7-0.7c0-1.7,0-3.3,0-5c0-0.5-0.2-0.7-0.6-1
c-1.6-0.9-3.2-1.9-4.8-2.8c-0.2-0.1-0.7-0.1-0.9,0c-1.6,0.9-3.2,1.9-4.8,2.8c-0.4,0.2-0.6,0.5-0.6,1c0,3.2,0,6.3,0,9.5
c0,1.9,0,1.9-1.9,1.9c-0.4,0-0.6-0.1-0.6-0.6c0-0.6,0-1.3,0-1.9c0-0.5-0.2-0.6-0.6-0.6c-1.1,0-2.2,0-3.3,0c-0.5,0-0.7,0.2-0.7,0.7
c0,1.6,0,3.3,0,4.9c0,0.5,0.2,0.8,0.6,1c1.6,0.9,3.2,1.9,4.8,2.8c0.2,0.1,0.7,0.1,0.9,0c1.6-0.9,3.2-1.9,4.8-2.8
c0.4-0.2,0.6-0.5,0.6-1c0-3.1,0-6.1,0-9.2c0-1.9,0-1.9,1.9-1.9c0.5,0,0.7,0.2,0.7,0.7C20.8,13.3,20.8,14,20.8,14.8z"/>
<path id="XMLID_655_" class="st0" d="M18,2.1l-6.8,3.9V2.7c0-0.3,0.3-0.6,0.6-0.6H18z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import path from 'path'
import {
CompileTree,
createFile,
loadDependenciesFile,
readFile,
@@ -17,11 +18,12 @@ const compiledSystemInit = async (systemInit: string) =>
programFolders: [],
macroFolders: [],
buildSourceFolder: '',
macroCorePath
binaryFolders: [],
macroCorePath,
compileTree: new CompileTree('') // dummy compileTree
}))
const createSysInitFile = async () => {
console.log('macroCorePath', macroCorePath)
const systemInitContent = await readFile(
path.join(__dirname, 'systemInit.sas')
)

View File

@@ -0,0 +1,32 @@
import path from 'path'
import {
asyncForEach,
copy,
createFile,
createFolder,
deleteFolder,
listFilesInFolder
} from '@sasjs/utils'
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
export const copySASjsCore = async () => {
await deleteFolder(sasJSCoreMacros)
await createFolder(sasJSCoreMacros)
const foldersToCopy = ['base', 'ddl', 'fcmp', 'lua', 'server']
await asyncForEach(foldersToCopy, async (coreSubFolder) => {
const coreSubFolderPath = path.join(macroCorePath, coreSubFolder)
await copy(coreSubFolderPath, sasJSCoreMacros)
})
const fileNames = await listFilesInFolder(sasJSCoreMacros)
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
}
copySASjsCore()

View File

@@ -4,10 +4,13 @@
@details This program is inserted into every sasjs/server program invocation,
_before_ any user-provided content.
<h4> SAS Macros </h4>
@li mcf_stpsrv_header.sas
A number of useful CORE macros are also compiled below, so that they can be
available by default for Stored Programs.
Note that the full CORE library is available to sessions in SASjs Studio.
<h4> SAS Macros </h4>
@li mfs_httpheader.sas
@li ms_webout.sas
**/
%mcf_stpsrv_header(wrap=YES, insert_cmplib=YES)

View File

@@ -1,33 +1,156 @@
import path from 'path'
import express from 'express'
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 webRouter from './routes/web'
import apiRouter from './routes/api'
import { connectDB, getWebBuildFolderPath } from './utils'
import {
connectDB,
copySASjsCore,
CorsType,
getWebBuildFolder,
HelmetCoepType,
instantiateLogger,
loadAppStreamConfig,
ModeType,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders,
verifyEnvVariables
} from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
dotenv.config()
instantiateLogger()
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
const { MODE, CORS, PORT_WEB } = process.env
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
console.log('All CORS Requests are enabled')
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(
cors({ credentials: true, origin: `http://localhost:${PORT_WEB ?? 3000}` })
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: '50mb' }))
app.use(morgan('tiny'))
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
app.use('/', webRouter)
app.use('/SASjsApi', apiRouter)
app.use(express.json({ limit: '50mb' }))
const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
app.use(express.static(getWebBuildFolderPath()))
console.error(err.stack)
res.status(500).send('Something broke!')
}
export default connectDB().then(() => app)
export default setProcessVariables().then(async () => {
await setupFolders()
await copySASjsCore()
// loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc
const { setupRoutes } = await import('./routes/setupRoutes')
setupRoutes(app)
await loadAppStreamConfig()
// should be served after setting up web route
// index.html needs to be injected with some js script.
app.use(express.static(getWebBuildFolder()))
app.use(onError)
return app
})

View File

@@ -1,10 +1,8 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import jwt from 'jsonwebtoken'
import User from '../model/User'
import { InfoJWT } from '../types'
import {
generateAccessToken,
generateAuthCode,
generateRefreshToken,
removeTokensInDB,
saveTokensInDB
@@ -24,20 +22,6 @@ export class AuthController {
static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId]
/**
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
*
*/
@Example<AuthorizeResponse>({
code: 'someRandomCryptoString'
})
@Post('/authorize')
public async authorize(
@Body() body: AuthorizePayload
): Promise<AuthorizeResponse> {
return authorize(body)
}
/**
* @summary Accepts client/auth code and returns access/refresh tokens
*
@@ -78,30 +62,6 @@ export class AuthController {
}
}
const authorize = async (data: any): Promise<AuthorizeResponse> => {
const { username, password, clientId } = data
// 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.')
// generate authorization code against clientId
const userInfo: InfoJWT = {
clientId,
userId: user.id
}
const code = AuthController.saveCode(
user.id,
clientId,
generateAuthCode(userInfo)
)
return { code }
}
const token = async (data: any): Promise<TokenResponse> => {
const { clientId, code } = data
@@ -139,32 +99,6 @@ const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
interface AuthorizePayload {
/**
* Username for user
* @example "secretuser"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
/**
* Client ID
* @example "clientID1"
*/
clientId: string
}
interface AuthorizeResponse {
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}
interface TokenPayload {
/**
* Client ID

View File

@@ -0,0 +1,78 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.'
import {
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray,
RunTimeType
} from '../utils'
interface ExecuteCodePayload {
/**
* Code of program
* @example "* Code HERE;"
*/
code: string
/**
* runtime for program
* @example "js"
*/
runTime: RunTimeType
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
*/
@Post('/execute')
public async executeCode(
@Request() request: express.Request,
@Body() body: ExecuteCodePayload
): Promise<ExecuteReturnJsonResponse> {
return executeCode(request, body)
}
}
const executeCode = async (
req: express.Request,
{ code, runTime }: ExecuteCodePayload
) => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: 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
return {
status: 'success',
_webout: webout as string,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -1,5 +1,8 @@
import path from 'path'
import express, { Express } from 'express'
import {
Security,
Request,
Route,
Tags,
Example,
@@ -8,35 +11,39 @@ import {
Response,
Query,
Get,
Patch
Patch,
UploadedFile,
FormField,
Delete,
Hidden
} from 'tsoa'
import { fileExists, readFile, createFile } from '@sasjs/utils'
import {
fileExists,
moveFile,
createFolder,
deleteFile as deleteFileOnSystem,
folderExists,
listFilesInFolder,
listSubFoldersInFolder,
isFolder,
FileTree,
isFileTree
} from '@sasjs/utils'
import { createFileTree, ExecutionController, getTreeExample } from './internal'
import { FileTree, isFileTree, TreeNode } from '../types'
import path from 'path'
import { getTmpFilesFolderPath } from '../utils'
import { TreeNode } from '../types'
import { getFilesFolder } from '../utils'
interface DeployPayload {
appLoc?: string
appLoc: string
streamWebFolder?: string
fileTree: FileTree
}
interface FilePayload {
/**
* Path of the file
* @example "/Public/somefolder/some.file"
*/
filePath: string
/**
* Contents of the file
* @example "Contents of the File"
*/
fileContent: string
}
interface DeployResponse {
status: string
message: string
streamServiceName?: string
example?: FileTree
}
@@ -89,57 +96,111 @@ export class DriveController {
}
/**
* @summary Get file from SASjs Drive
* @query filePath Location of SAS program
* @example filePath "/Public/somefolder/some.file"
* Accepts JSON file and zipped compressed JSON file as well.
* Compressed file should only contain one JSON file and should have same name
* as of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip
* Any other file or JSON file in zipped will be ignored!
*
* @summary Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.
*
*/
@Example<GetFileResponse>({
status: 'success',
fileContent: 'Contents of the File'
})
@Response<GetFileResponse>(400, 'Unable to get File', {
status: 'failure',
message: 'File request failed.'
})
@Get('/file')
public async getFile(@Query() filePath: string): Promise<GetFileResponse> {
return getFile(filePath)
@Example<DeployResponse>(successDeployResponse)
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
@Post('/deploy/upload')
public async deployUpload(
@UploadedFile() file: Express.Multer.File, // passing here for API docs
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
): Promise<DeployResponse> {
return deploy(body!)
}
/**
*
* @summary Get file from SASjs Drive
* @query _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file"
*/
@Get('/file')
public async getFile(
@Request() request: express.Request,
@Query() _filePath: string
) {
return getFile(request, _filePath)
}
/**
*
* @summary Get folder contents from SASjs Drive
* @query _folderPath Location of SAS program
* @example _folderPath "/Public/somefolder"
*/
@Get('/folder')
public async getFolder(@Query() _folderPath?: string) {
return getFolder(_folderPath)
}
/**
*
* @summary Delete file from SASjs Drive
* @query _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file"
*/
@Delete('/file')
public async deleteFile(@Query() _filePath: string) {
return deleteFile(_filePath)
}
/**
* 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
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(400, 'File already exists', {
@Response<UpdateFileResponse>(403, 'File already exists', {
status: 'failure',
message: 'File request failed.'
})
@Post('/file')
public async saveFile(
@Body() body: FilePayload
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
return saveFile(body)
return saveFile((_filePath ?? filePath)!, file)
}
/**
* 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 Modify a file in SASjs Drive
* @param _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(400, 'Unable to get File', {
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
status: 'failure',
message: 'File request failed.'
})
@Patch('/file')
public async updateFile(
@Body() body: FilePayload
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
return updateFile(body)
return updateFile((_filePath ?? filePath)!, file)
}
/**
@@ -153,91 +214,151 @@ export class DriveController {
}
const getFileTree = () => {
const tree = new ExecutionController().buildDirectorytree()
const tree = new ExecutionController().buildDirectoryTree()
return { status: 'success', tree }
}
const deploy = async (data: DeployPayload) => {
const driveFilesPath = getFilesFolder()
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
const appLocPath = path
.join(getFilesFolder(), ...appLocParts)
.replace(new RegExp('/', 'g'), path.sep)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
if (!isFileTree(data.fileTree)) {
throw { code: 400, ...invalidDeployFormatResponse }
}
await createFileTree(
data.fileTree.members,
data.appLoc ? data.appLoc.replace(/^\//, '').split('/') : []
).catch((err) => {
await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
throw { code: 500, ...execDeployErrorResponse, ...err }
})
return successDeployResponse
}
const getFile = async (filePath: string): Promise<GetFileResponse> => {
try {
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
const getFile = async (req: express.Request, filePath: string) => {
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot get file outside drive.')
}
if (!(await fileExists(filePathFull))) {
throw new Error("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))
}
const getFolder = async (folderPath?: string) => {
const driveFilesPath = getFilesFolder()
if (folderPath) {
const folderPathFull = path
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
await validateFilePath(filePathFull)
const fileContent = await readFile(filePathFull)
return { status: 'success', fileContent: fileContent }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'File request failed.',
error: typeof err === 'object' ? err.toString() : err
if (!folderPathFull.includes(driveFilesPath)) {
throw new Error('Cannot get folder outside drive.')
}
if (!(await folderExists(folderPathFull))) {
throw new Error("Folder doesn't exist.")
}
if (!(await isFolder(folderPathFull))) {
throw new Error('Not a Folder.')
}
const files: string[] = await listFilesInFolder(folderPathFull)
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
return { files, folders }
}
const files: string[] = await listFilesInFolder(driveFilesPath)
const folders: string[] = await listSubFoldersInFolder(driveFilesPath)
return { files, folders }
}
const saveFile = async (body: FilePayload): Promise<GetFileResponse> => {
const { filePath, fileContent } = body
try {
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
const deleteFile = async (filePath: string) => {
const driveFilesPath = getFilesFolder()
if (await fileExists(filePathFull)) {
throw 'DriveController: File already exists.'
}
await createFile(filePathFull, fileContent)
const filePathFull = path
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
return { status: 'success' }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'File request failed.',
error: typeof err === 'object' ? err.toString() : err
}
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot delete file outside drive.')
}
if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.')
}
await deleteFileOnSystem(filePathFull)
return { status: 'success' }
}
const updateFile = async (body: FilePayload): Promise<GetFileResponse> => {
const { filePath, fileContent } = body
try {
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
const saveFile = async (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getFilesFolder()
await validateFilePath(filePathFull)
await createFile(filePathFull, fileContent)
const filePathFull = path
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
return { status: 'success' }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'File request failed.',
error: typeof err === 'object' ? err.toString() : err
}
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot put file outside drive.')
}
if (await fileExists(filePathFull)) {
throw new Error('File already exists.')
}
const folderPath = path.dirname(filePathFull)
await createFolder(folderPath)
await moveFile(multerFile.path, filePathFull)
return { status: 'success' }
}
const validateFilePath = async (filePath: string) => {
if (!(await fileExists(filePath))) {
throw 'DriveController: File does not exists.'
const updateFile = async (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot modify file outside drive.')
}
if (!(await fileExists(filePathFull))) {
throw new Error(`File doesn't exist.`)
}
await moveFile(multerFile.path, filePathFull)
return { status: 'success' }
}

View File

@@ -14,7 +14,7 @@ import Group, { GroupPayload } from '../model/Group'
import User from '../model/User'
import { UserResponse } from './user'
interface GroupResponse {
export interface GroupResponse {
groupId: number
name: string
description: string
@@ -28,6 +28,11 @@ interface GroupDetailsResponse {
users: UserResponse[]
}
interface GetGroupBy {
groupId?: number
name?: string
}
@Security('bearerAuth')
@Route('SASjsApi/group')
@Tags('Group')
@@ -66,6 +71,18 @@ export class GroupController {
return createGroup(body)
}
/**
* @summary Get list of members of a group (userName). All users can request this.
* @param name The group's name
* @example dcgroup
*/
@Get('by/groupname/{name}')
public async getGroupByGroupName(
@Path() name: string
): Promise<GroupDetailsResponse> {
return getGroup({ name })
}
/**
* @summary Get list of members of a group (userName). All users can request this.
* @param groupId The group's identifier
@@ -75,7 +92,7 @@ export class GroupController {
public async getGroup(
@Path() groupId: number
): Promise<GroupDetailsResponse> {
return getGroup(groupId)
return getGroup({ groupId })
}
/**
@@ -129,9 +146,13 @@ export class GroupController {
*/
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const { deletedCount } = await Group.deleteOne({ groupId })
if (deletedCount) return
throw new Error('No Group deleted!')
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
}
}
@@ -145,6 +166,15 @@ const createGroup = async ({
description,
isActive
}: GroupPayload): Promise<GroupDetailsResponse> => {
// Checking if user is already in the database
const groupnameExist = await Group.findOne({ name })
if (groupnameExist)
throw {
code: 409,
status: 'Conflict',
message: 'Group name already exists.'
}
const group = new Group({
name,
description,
@@ -162,15 +192,20 @@ const createGroup = async ({
}
}
const getGroup = async (groupId: number): Promise<GroupDetailsResponse> => {
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne(
{ groupId },
findBy,
'groupId name description isActive users -_id'
).populate(
'users',
'id username displayName -_id'
)) as unknown as GroupDetailsResponse
if (!group) throw new Error('Group not found.')
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return {
groupId: group.groupId,
@@ -199,16 +234,34 @@ const updateUsersListInGroup = async (
action: 'addUser' | 'removeUser'
): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ groupId })
if (!group) throw new Error('Group not found.')
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
const user = await User.findOne({ id: userId })
if (!user) throw new Error('User not found.')
if (!user)
throw {
code: 404,
status: 'Not Found',
message: 'User not found.'
}
const updatedGroup = (action === 'addUser'
? await group.addUser(user._id)
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
if (!updatedGroup) throw new Error('Unable to update group')
if (!updatedGroup)
throw {
code: 400,
status: 'Bad Request',
message: 'Unable to update group.'
}
if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id)
return {
groupId: updatedGroup.groupId,

View File

@@ -1,6 +1,10 @@
export * from './auth'
export * from './client'
export * from './code'
export * from './drive'
export * from './group'
export * from './info'
export * from './session'
export * from './stp'
export * from './user'
export * from './web'

View File

@@ -0,0 +1,39 @@
import { Route, Tags, Example, Get } from 'tsoa'
export interface InfoResponse {
mode: string
cors: string
whiteList: string[]
protocol: string
runTimes: string[]
}
@Route('SASjsApi/info')
@Tags('Info')
export class InfoController {
/**
* @summary Get server info (mode, cors, whiteList, protocol).
*
*/
@Example<InfoResponse>({
mode: 'desktop',
cors: 'enable',
whiteList: ['http://example.com', 'http://example2.com'],
protocol: 'http',
runTimes: ['sas', 'js']
})
@Get('/')
public info(): InfoResponse {
const response = {
mode: process.env.MODE ?? 'desktop',
cors:
process.env.CORS ||
(process.env.MODE === 'server' ? 'disable' : 'enable'),
whiteList:
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
protocol: process.env.PROTOCOL ?? 'http',
runTimes: process.runTimes
}
return response
}
}

View File

@@ -1,132 +1,155 @@
import path from 'path'
import fs from 'fs'
import { getSessionController } from './'
import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, TreeNode } from '../../types'
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
import {
getSASSessionController,
getJSSessionController,
processProgram
} from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types'
import {
extractHeaders,
getFilesFolder,
HTTPHeaders,
isDebugOn,
RunTimeType
} from '../../utils'
export interface ExecutionVars {
[key: string]: string | number | undefined
}
export interface ExecuteReturnRaw {
httpHeaders: HTTPHeaders
result: string | Buffer
}
export interface ExecuteReturnJson {
httpHeaders: HTTPHeaders
webout: string | Buffer
log?: string
}
interface ExecuteFileParams {
programPath: string
preProgramVariables: PreProgramVars
vars: ExecutionVars
otherArgs?: any
returnJson?: boolean
session?: Session
runTime: RunTimeType
}
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string
}
export class ExecutionController {
async execute(
programPath: string,
preProgramVariables: PreProgramVars,
vars: { [key: string]: string | number | undefined },
otherArgs?: any,
returnJson?: boolean
) {
if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.'
async executeFile({
programPath,
preProgramVariables,
vars,
otherArgs,
returnJson,
session,
runTime
}: ExecuteFileParams) {
const program = await readFile(programPath)
let program = await readFile(programPath)
const sessionController = getSessionController()
const session = await sessionController.getSession()
session.inUse = true
const logPath = path.join(session.path, 'log.log')
const weboutPath = path.join(session.path, 'webout.txt')
await createFile(weboutPath, '')
const tokenFile = path.join(session.path, 'accessToken.txt')
await createFile(
tokenFile,
preProgramVariables?.accessToken ?? 'accessToken'
)
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) =>
`${computed}%let ${key}=${vars[key]};\n`,
''
)
const preProgramVarStatments = `
%let _sasjs_tokenfile=${tokenFile};
%let _sasjs_username=${preProgramVariables?.username};
%let _sasjs_userid=${preProgramVariables?.userId};
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
%let sasjsprocessmode=Stored Program;`
program = `
/* runtime vars */
${varStatments}
filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */
${preProgramVarStatments}
/* actual job code */
${program}`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs && otherArgs.filesNamesMap) {
const uploadSasCode = await generateFileUploadSasCode(
otherArgs.filesNamesMap,
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}
}
const codePath = path.join(session.path, 'code.sas')
// Creating this file in a RUNNING session will break out
// the autoexec loop and actually execute the program
// but - given it will take several milliseconds to create
// (which can mean SAS trying to run a partial program, or
// failing due to file lock) we first create the file THEN
// we rename it.
await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session array
while (!session.completed) {
await delay(50)
}
const log =
((await fileExists(logPath)) ? await readFile(logPath) : '') +
session.crashed
const webout = (await fileExists(weboutPath))
? await readFile(weboutPath)
: ''
const debugValue =
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
let debugResponse: string | undefined
if ((debugValue && debugValue >= 131) || session.crashed) {
debugResponse = `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
}
session.inUse = false
sessionController.deleteSession(session)
if (returnJson) {
const response: any = {
webout: webout
}
if ((debugValue && debugValue >= 131) || session.crashed) {
response.log = log
}
return response
}
return debugResponse ?? webout
return this.executeProgram({
program,
preProgramVariables,
vars,
otherArgs,
returnJson,
session,
runTime
})
}
buildDirectorytree() {
async executeProgram({
program,
preProgramVariables,
vars,
otherArgs,
returnJson,
session: sessionByFileUpload,
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
const session =
sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true
session.consumed = true
const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt')
const weboutPath = path.join(session.path, 'webout.txt')
const tokenFile = path.join(session.path, 'reqHeaders.txt')
await createFile(weboutPath, '')
await createFile(
tokenFile,
preProgramVariables?.httpHeaders.join('\n') ?? ''
)
await processProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
runTime,
logPath,
otherArgs
)
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
const headersContent = (await fileExists(headersPath))
? 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
const webout = (await fileExists(weboutPath))
? fileResponse
? await readFileBinary(weboutPath)
: await readFile(weboutPath)
: ''
// it should be deleted by scheduleSessionDestroy
session.inUse = false
if (returnJson) {
return {
httpHeaders,
webout,
log: isDebugOn(vars) || session.crashed ? log : undefined
}
}
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>`
: webout
}
}
buildDirectoryTree() {
const root: TreeNode = {
name: 'files',
relativePath: '',
absolutePath: getTmpFilesFolderPath(),
absolutePath: getFilesFolder(),
children: []
}
@@ -159,5 +182,3 @@ ${program}`
return root
}
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -1,14 +1,20 @@
import { Request, RequestHandler } from 'express'
import multer from 'multer'
import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.'
const multer = require('multer')
import { getSASSessionController, getJSSessionController } from '.'
import {
executeProgramRawValidation,
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
export class FileUploadController {
private storage = multer.diskStorage({
destination: function (req: any, file: any, cb: any) {
destination: function (req: Request, file: any, cb: any) {
//Sending the intercepted files to the sessions subfolder
cb(null, req.sasSession.path)
cb(null, req.sasjsSession?.path)
},
filename: function (req: any, file: any, cb: any) {
filename: function (req: Request, file: any, cb: any) {
//req_file prefix + unique hash added to sas request files
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
}
@@ -18,14 +24,37 @@ export class FileUploadController {
//It will intercept request and generate unique uuid to be used as a subfolder name
//that will store the files uploaded
public preuploadMiddleware = async (req: any, res: any, next: any) => {
let session
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
const { error: errB, value: body } = executeProgramRawValidation(req.body)
const sessionController = getSessionController()
session = await sessionController.getSession()
session.inUse = true
if (errQ && errB) return res.status(400).send(errB.details[0].message)
req.sasSession = session
const programPath = (query?._program ?? body?._program) as string
let runTime
try {
;({ runTime } = await getRunTimeAndFilePath(programPath))
} catch (err: any) {
res.status(400).send({
status: 'failure',
message: 'Job execution failed',
error: typeof err === 'object' ? err.toString() : err
})
}
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
const session = await sessionController.getSession()
// marking consumed true, so that it's not available
// as readySession for any other request
session.consumed = true
req.sasjsSession = session
next()
}

View File

@@ -3,7 +3,7 @@ import { Session } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getTmpSessionsFolderPath,
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath
} from '../../utils'
@@ -17,26 +17,34 @@ import {
const execFilePromise = promisify(execFile)
export class SessionController {
private sessions: Session[] = []
abstract class SessionController {
protected sessions: Session[] = []
protected getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
protected abstract createSession(): Promise<Session>
public async getSession() {
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
const readySessions = this.getReadySessions()
const session = readySessions.length
? readySessions[0]
: await this.createSession()
if (readySessions.length < 2) this.createSession()
if (readySessions.length < 3) this.createSession()
return session
}
}
private async createSession() {
export class SASSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
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 -
@@ -47,6 +55,7 @@ export class SessionController {
id: sessionId,
ready: false,
inUse: false,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
@@ -62,7 +71,10 @@ export class SessionController {
// the autoexec file is executed on SAS startup
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
const contentForAutoExec = `/* compiled systemInit */\n${compiledSystemInitContent}\n/* autoexec */\n${autoExecContent}`
const contentForAutoExec = `/* compiled systemInit */
${compiledSystemInitContent}
/* autoexec */
${autoExecContent}`
await createFile(autoExecPath, contentForAutoExec)
// create empty code.sas as SAS will not start without a SYSIN
@@ -74,16 +86,24 @@ export class SessionController {
// however we also need a promise so that we can update the
// session array to say that it has (eventually) finished.
// Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc, [
'-SYSIN',
codePath,
'-LOG',
path.join(session.path, 'log.log'),
'-PRINT',
path.join(session.path, 'output.lst'),
'-WORK',
session.path,
'-AUTOEXEC',
autoExecPath,
process.platform === 'win32' ? '-nosplash' : ''
'-ENCODING',
'UTF-8',
process.platform === 'win32' ? '-nosplash' : '',
process.platform === 'win32' ? '-icon' : '',
process.platform === 'win32' ? '-nologo' : ''
])
.then(() => {
session.completed = true
@@ -92,7 +112,7 @@ export class SessionController {
.catch((err) => {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id)
console.log('session crashed', session.id, session.crashed)
})
// we have a triggered session - add to array
@@ -105,15 +125,16 @@ export class SessionController {
return session
}
public async waitForSession(session: Session) {
private async waitForSession(session: Session) {
const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {}
console.log('session crashed?', !!session.crashed, session.crashed)
if (session.crashed)
console.log('session crashed! while waiting to be ready', session.crashed)
session.ready = true
return Promise.resolve(session)
}
public async deleteSession(session: Session) {
@@ -121,17 +142,17 @@ export class SessionController {
await deleteFolder(session.path)
// remove the session from the session array
if (session.ready) {
this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id
)
}
this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id
)
}
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
if (session.inUse) {
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
// adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session)
} else {
@@ -141,18 +162,59 @@ export class SessionController {
}
}
export const getSessionController = (): SessionController => {
if (process.sessionController) return process.sessionController
export class JSSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
process.sessionController = new SessionController()
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()
return process.sessionController
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
}
}
export const getSASSessionController = (): SASSessionController => {
if (process.sasSessionController) return process.sasSessionController
process.sasSessionController = new SASSessionController()
return process.sasSessionController
}
export const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController()
return process.jsSessionController
}
const autoExecContent = `
data _null_;
/* remove the dummy SYSIN */
length fname $8;
call missing(fname);
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);

View File

@@ -0,0 +1,64 @@
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './'
export const createJSProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) =>
`${computed}const ${key} = '${vars[key]}';\n`,
''
)
const preProgramVarStatments = `
let _webout = '';
const weboutPath = '${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 requiredModules = `const fs = require('fs')`
program = `
/* runtime vars */
${varStatments}
/* dynamic user-provided vars */
${preProgramVarStatments}
/* actual job code */
${program}
/* write webout file only if webout exists*/
if (_webout) {
fs.writeFile(weboutPath, _webout, function (err) {
if (err) throw err;
})
}
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
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
}
}
return requiredModules + program
}

View File

@@ -0,0 +1,69 @@
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadSasCode, getMacrosFolder } from '../../utils'
import { ExecutionVars } from './'
export const createSASProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => `${computed}%let ${key}=${vars[key]};\n`,
''
)
const preProgramVarStatments = `
%let _sasjs_tokenfile=${tokenFile};
%let _sasjs_username=${preProgramVariables?.username};
%let _sasjs_userid=${preProgramVariables?.userId};
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
%let sasjsprocessmode=Stored Program;
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
%macro _sasjs_server_init();
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
%mend;
%_sasjs_server_init()
`
program = `
options insert=(SASAUTOS="${getMacrosFolder()}");
/* runtime vars */
${varStatments}
filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */
${preProgramVarStatments}
/* user autoexec starts */
${otherArgs?.userAutoExec ?? ''}
/* user autoexec ends */
/* actual job code */
${program}`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadSasCode = await generateFileUploadSasCode(
otherArgs.filesNamesMap,
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}
}
return program
}

View File

@@ -1,37 +1,52 @@
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
import { getTmpFilesFolderPath } from '../../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
import path from 'path'
import { getFilesFolder } from '../../utils/file'
import {
createFolder,
createFile,
asyncForEach,
FolderMember,
ServiceMember,
FileMember,
MemberType,
FileTree
} from '@sasjs/utils'
// REFACTOR: export FileTreeCpntroller
export const createFileTree = async (
members: (FolderMember | ServiceMember)[],
members: (FolderMember | ServiceMember | FileMember)[],
parentFolders: string[] = []
) => {
const destinationPath = path.join(
getTmpFilesFolderPath(),
getFilesFolder(),
path.join(...parentFolders)
)
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
let name = member.name
await asyncForEach(
members,
async (member: FolderMember | ServiceMember | FileMember) => {
let name = member.name
if (member.type === MemberType.service) name += '.sas'
if (member.type === MemberType.service) name += '.sas'
if (member.type === MemberType.folder) {
await createFolder(path.join(destinationPath, name)).catch((err) =>
Promise.reject({ error: err, failedToCreate: name })
)
if (member.type === MemberType.folder) {
await createFolder(path.join(destinationPath, name)).catch((err) =>
Promise.reject({ error: err, failedToCreate: name })
)
await createFileTree(member.members, [...parentFolders, name]).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
} else {
await createFile(path.join(destinationPath, name), member.code).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
await createFileTree(member.members, [...parentFolders, name]).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
} else {
const encoding = member.type === MemberType.file ? 'base64' : undefined
await createFile(
path.join(destinationPath, name),
member.code,
encoding
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
}
}
})
)
return Promise.resolve()
}

View File

@@ -2,3 +2,6 @@ export * from './deploy'
export * from './Session'
export * from './Execution'
export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './processProgram'

View File

@@ -0,0 +1,86 @@
import path from 'path'
import fs from 'fs'
import { execFileSync } from 'child_process'
import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { RunTimeType } from '../../utils'
import { ExecutionVars, createSASProgram, createJSProgram } from './'
export const processProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: 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 {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.sas')
// Creating this file in a RUNNING session will break out
// the autoexec loop and actually execute the program
// but - given it will take several milliseconds to create
// (which can mean SAS trying to run a partial program, or
// failing due to file lock) we first create the file THEN
// we rename it.
await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status
while (!session.completed) {
await delay(50)
}
}
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -0,0 +1,30 @@
import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user'
@Security('bearerAuth')
@Route('SASjsApi/session')
@Tags('Session')
export class SessionController {
/**
* @summary Get session info (username).
*
*/
@Example<UserResponse>({
id: 123,
username: 'johnusername',
displayName: 'John'
})
@Get('/')
public async session(
@Request() request: express.Request
): Promise<UserResponse> {
return session(request)
}
}
const session = (req: express.Request) => ({
id: req.user!.userId,
username: req.user!.username,
displayName: req.user!.displayName
})

View File

@@ -1,19 +1,31 @@
import express, { response } from 'express'
import path from 'path'
import express from 'express'
import {
Request,
Security,
Route,
Tags,
Example,
Post,
Body,
Get,
Query
Query,
Example
} from 'tsoa'
import { ExecutionController } from './internal'
import { PreProgramVars } from '../types'
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
import {
ExecuteReturnJson,
ExecuteReturnRaw,
ExecutionController,
ExecutionVars
} from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray,
getRunTimeAndFilePath
} from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload {
/**
@@ -22,11 +34,16 @@ interface ExecuteReturnJsonPayload {
*/
_program?: string
}
interface ExecuteReturnJsonResponse {
interface IRecordOfAny {
[key: string]: any
}
export interface ExecuteReturnJsonResponse {
status: string
log?: string
_webout?: string
_webout: string | IRecordOfAny
log: LogLine[]
message?: string
httpHeaders: HTTPHeaders
}
@Security('bearerAuth')
@@ -34,32 +51,51 @@ interface ExecuteReturnJsonResponse {
@Tags('STP')
export class STPController {
/**
* Trigger a SAS program using it's location in the _program parameter.
* Enable debugging using the _debug parameter.
* Additional URL parameters are turned into SAS macro variables.
* Any files provided are placed into the session and
* corresponding _WEBIN_XXX variables are created.
* @summary Execute Stored Program, return raw content
* @query _program Location of SAS program
* @example _program "/Public/somefolder/some.file"
* Trigger a SAS or JS program using the _program URL parameter.
*
* Accepts URL parameters 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
* @example _program "/Projects/myApp/some/program"
*/
@Get('/execute')
public async executeReturnRaw(
@Request() request: express.Request,
@Query() _program: string
): Promise<string> {
): Promise<string | Buffer> {
return executeReturnRaw(request, _program)
}
/**
* Trigger a SAS program using it's location in the _program parameter.
* Enable debugging using the _debug parameter.
* Additional URL parameters are turned into SAS macro variables.
* Any files provided are placed into the session and
* corresponding _WEBIN_XXX variables are created.
* @summary Execute Stored Program, return JSON
* @query _program Location of SAS program
* @example _program "/Public/somefolder/some.file"
* Trigger a SAS or JS 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
* @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(
@Request() request: express.Request,
@@ -74,21 +110,33 @@ export class STPController {
const executeReturnRaw = async (
req: express.Request,
_program: string
): Promise<string> => {
const query = req.query as { [key: string]: string | number | undefined }
const sasCodePath =
path
.join(getTmpFilesFolderPath(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
): Promise<string | Buffer> => {
const query = req.query as ExecutionVars
try {
const result = await new ExecutionController().execute(
sasCodePath,
getPreProgramVariables(req),
query
)
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
return result as string
const { result, httpHeaders } =
(await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: query,
runTime
})) as ExecuteReturnRaw
// 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)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,
@@ -100,28 +148,39 @@ const executeReturnRaw = async (
}
const executeReturnJson = async (
req: any,
req: express.Request,
_program: string
): Promise<ExecuteReturnJsonResponse> => {
const sasCodePath =
path
.join(getTmpFilesFolderPath(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
try {
const jsonResult: any = await new ExecutionController().execute(
sasCodePath,
getPreProgramVariables(req),
{ ...req.query, ...req.body },
{ filesNamesMap: filesNamesMap },
true
)
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
let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
try {
weboutRes = JSON.parse(webout as string)
} catch (_) {}
}
return {
status: 'success',
_webout: jsonResult.webout,
log: jsonResult.log
_webout: weboutRes,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {
@@ -132,16 +191,3 @@ const executeReturnJson = async (
}
}
}
const getPreProgramVariables = (req: any): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
serverUrl: protocol + host,
accessToken
}
}

View File

@@ -1,3 +1,4 @@
import express from 'express'
import {
Security,
Route,
@@ -10,10 +11,14 @@ import {
Patch,
Delete,
Body,
Hidden
Hidden,
Request
} from 'tsoa'
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import { GroupResponse } from './group'
export interface UserResponse {
id: number
@@ -27,6 +32,8 @@ interface UserDetailsResponse {
username: string
isActive: boolean
isAdmin: boolean
autoExec?: string
groups?: GroupResponse[]
}
@Security('bearerAuth')
@@ -73,13 +80,68 @@ export class UserController {
}
/**
* Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName.
* @param username The User's username
* @example username "johnSnow01"
*/
@Get('by/username/{username}')
public async getUserByUsername(
@Request() req: express.Request,
@Path() username: string
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req
const getAutoExec = user!.isAdmin || user!.username == username
return getUser({ username }, getAutoExec)
}
/**
* Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName.
* @param userId The user's identifier
* @example userId 1234
*/
@Get('{userId}')
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
return getUser(userId)
public async getUser(
@Request() req: express.Request,
@Path() userId: number
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req
const getAutoExec = user!.isAdmin || user!.userId == userId
return getUser({ id: userId }, getAutoExec)
}
/**
* @summary Update user properties - such as displayName. Can be performed either by admins, or the user in question.
* @param username The User's username
* @example username "johnSnow01"
*/
@Example<UserDetailsResponse>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Patch('by/username/{username}')
public async updateUserByUsername(
@Path() username: string,
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser({ username }, body)
}
/**
@@ -99,7 +161,26 @@ export class UserController {
@Path() userId: number,
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
return updateUser(userId, body)
const { MODE } = process.env
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser({ id: userId }, body)
}
/**
* @summary Delete a user. Can be performed either by admins, or the user in question.
* @param username The User's username
* @example username "johnSnow01"
*/
@Delete('by/username/{username}')
public async deleteUserByUsername(
@Path() username: string,
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser({ username }, isAdmin, body)
}
/**
@@ -113,7 +194,7 @@ export class UserController {
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser(userId, isAdmin, body)
return deleteUser({ id: userId }, isAdmin, body)
}
}
@@ -123,7 +204,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
.exec()
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data
const { displayName, username, password, isAdmin, isActive, autoExec } = data
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
@@ -138,7 +219,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
username,
password: hashPassword,
isAdmin,
isActive
isActive,
autoExec
})
const savedUser = await user.save()
@@ -148,38 +230,67 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
displayName: savedUser.displayName,
username: savedUser.username,
isActive: savedUser.isActive,
isAdmin: savedUser.isAdmin
isAdmin: savedUser.isAdmin,
autoExec: savedUser.autoExec
}
}
const getUser = async (id: number): Promise<UserDetailsResponse> => {
const user = await User.findOne({ id })
.select({
_id: 0,
id: 1,
username: 1,
displayName: 1,
isAdmin: 1,
isActive: 1
})
.exec()
interface GetUserBy {
id?: number
username?: string
}
const getUser = async (
findBy: GetUserBy,
getAutoExec: boolean
): Promise<UserDetailsResponse> => {
const user = (await User.findOne(
findBy,
`id displayName username isActive isAdmin autoExec -_id`
).populate(
'groups',
'groupId name description -_id'
)) as unknown as UserDetailsResponse
if (!user) throw new Error('User is not found.')
return user
return {
id: user.id,
displayName: user.displayName,
username: user.username,
isActive: user.isActive,
isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
groups: user.groups
}
}
const getDesktopAutoExec = async () => {
return {
...desktopUser,
id: desktopUser.userId,
autoExec: await getUserAutoExec()
}
}
const updateUser = async (
id: number,
data: UserPayload
findBy: GetUserBy,
data: Partial<UserPayload>
): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data
const { displayName, username, password, isAdmin, isActive, autoExec } = data
const params: any = { displayName, isAdmin, isActive }
const params: any = { displayName, isAdmin, isActive, autoExec }
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist?.id != id) throw new Error('Username already exists.')
if (usernameExist) {
if (
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw new Error('Username already exists.')
}
params.username = username
}
@@ -188,27 +299,36 @@ const updateUser = async (
params.password = User.hashPassword(password)
}
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
.select({
_id: 0,
id: 1,
username: 1,
displayName: 1,
isAdmin: 1,
isActive: 1
})
.exec()
if (!updatedUser) throw new Error('Unable to update user')
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
return updatedUser
if (!updatedUser)
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
return {
id: updatedUser.id,
username: updatedUser.username,
displayName: updatedUser.displayName,
isAdmin: updatedUser.isAdmin,
isActive: updatedUser.isActive,
autoExec: updatedUser.autoExec
}
}
const updateDesktopAutoExec = async (autoExec: string) => {
await updateUserAutoExec(autoExec)
return {
...desktopUser,
id: desktopUser.userId,
autoExec
}
}
const deleteUser = async (
id: number,
findBy: GetUserBy,
isAdmin: boolean,
{ password }: { password?: string }
) => {
const user = await User.findOne({ id })
const user = await User.findOne(findBy)
if (!user) throw new Error('User is not found.')
if (!isAdmin) {
@@ -216,5 +336,5 @@ const deleteUser = async (
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne({ id })
await User.deleteOne(findBy)
}

158
api/src/controllers/web.ts Normal file
View File

@@ -0,0 +1,158 @@
import path from 'path'
import express from 'express'
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
import { readFile } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import { getWebBuildFolder, generateAuthCode } from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@Route('/')
@Tags('Web')
export class WebController {
/**
* @summary Render index.html
*
*/
@Get('/')
public async home() {
return home()
}
/**
* @summary Accept a valid username/password
*
*/
@Post('/SASLogon/login')
public async login(
@Request() req: express.Request,
@Body() body: LoginPayload
) {
return login(req, body)
}
/**
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
*
*/
@Example<AuthorizeResponse>({
code: 'someRandomCryptoString'
})
@Post('/SASLogon/authorize')
public async authorize(
@Request() req: express.Request,
@Body() body: AuthorizePayload
): Promise<AuthorizeResponse> {
return authorize(req, body.clientId)
}
/**
* @summary Accept a valid username/password
*
*/
@Get('/logout')
public async logout(@Request() req: express.Request) {
return new Promise((resolve) => {
req.session.destroy(() => {
resolve(true)
})
})
}
}
const home = async () => {
const indexHtmlPath = path.join(getWebBuildFolder(), 'index.html')
// Attention! Cannot use fileExists here,
// due to limitation after building executable
const content = await readFile(indexHtmlPath)
return content
}
const login = async (
req: express.Request,
{ username, password }: LoginPayload
) => {
// 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.')
req.session.loggedIn = true
req.session.user = {
userId: user.id,
clientId: 'web_app',
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive,
autoExec: user.autoExec
}
return {
loggedIn: true,
user: {
id: user.id,
username: user.username,
displayName: user.displayName
}
}
}
const authorize = async (
req: express.Request,
clientId: string
): Promise<AuthorizeResponse> => {
const userId = req.session.user?.userId
if (!userId) throw new Error('Invalid userId.')
const client = await Client.findOne({ clientId })
if (!client) throw new Error('Invalid clientId.')
// generate authorization code against clientId
const userInfo: InfoJWT = {
clientId,
userId
}
const code = AuthController.saveCode(
userId,
clientId,
generateAuthCode(userInfo)
)
return { code }
}
interface LoginPayload {
/**
* Username for user
* @example "secretuser"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
}
interface AuthorizePayload {
/**
* Client ID
* @example "clientID1"
*/
clientId: string
}
interface AuthorizeResponse {
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}

View File

@@ -1,7 +1,36 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { verifyTokenInDB } from '../utils'
import { csrfProtection } from '../app'
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
import { desktopUser } from './desktop'
export const authenticateAccessToken: RequestHandler = async (
req,
res,
next
) => {
const { MODE } = process.env
if (MODE === ModeType.Desktop) {
req.user = desktopUser
return next()
}
// if request is coming from web and has valid session
// it can be validated.
if (req.session?.loggedIn) {
if (req.session.user) {
const user = await fetchLatestAutoExec(req.session.user)
if (user) {
if (user.isActive) {
req.user = user
return csrfProtection(req, res, next)
} else return res.sendStatus(401)
}
}
return res.sendStatus(401)
}
export const authenticateAccessToken = (req: any, res: any, next: any) => {
authenticateToken(
req,
res,
@@ -11,7 +40,7 @@ export const authenticateAccessToken = (req: any, res: any, next: any) => {
)
}
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
authenticateToken(
req,
res,
@@ -22,16 +51,16 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
}
const authenticateToken = (
req: any,
res: any,
next: any,
req: Request,
res: Response,
next: NextFunction,
key: string,
tokenType: 'accessToken' | 'refreshToken' = 'accessToken'
tokenType: 'accessToken' | 'refreshToken'
) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
req.user = {
userId: '1234',
userId: 1234,
clientId: 'desktopModeClientId',
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',

View File

@@ -0,0 +1,37 @@
import { RequestHandler, Request } from 'express'
import { userInfo } from 'os'
import { RequestUser } from '../types'
import { ModeType } from '../utils'
const regexUser = /^\/SASjsApi\/user\/[0-9]*$/ // /SASjsApi/user/1
const allowedInDesktopMode: { [key: string]: RegExp[] } = {
GET: [regexUser],
PATCH: [regexUser]
}
const reqAllowedInDesktopMode = (request: Request): boolean => {
const { method, originalUrl: url } = request
return !!allowedInDesktopMode[method]?.find((urlRegex) => urlRegex.test(url))
}
export const desktopRestrict: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE === ModeType.Desktop) {
if (!reqAllowedInDesktopMode(req))
return res.status(403).send('Not Allowed while in Desktop Mode.')
}
next()
}
export const desktopUser: RequestUser = {
userId: 12345,
clientId: 'desktop_app',
username: userInfo().username,
displayName: userInfo().username,
isAdmin: true,
isActive: true
}

View File

@@ -1,7 +0,0 @@
export const desktopRestrict = (req: any, res: any, next: any) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server')
return res.status(403).send('Not Allowed while in Desktop Mode.')
next()
}

View File

@@ -1,4 +1,4 @@
export * from './authenticateToken'
export * from './desktopRestrict'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'

View File

@@ -0,0 +1,72 @@
import path from 'path'
import { Request } from 'express'
import multer, { FileFilterCallback, Options } from 'multer'
import { blockFileRegex, getUploadsFolder } from '../utils'
const fieldNameSize = 300
const fileSize = 104857600 // 100 MB
const storage = multer.diskStorage({
destination: getUploadsFolder(),
filename: function (
_req: Request,
file: Express.Multer.File,
callback: (error: Error | null, filename: string) => void
) {
callback(
null,
file.fieldname + path.extname(file.originalname) + '-' + Date.now()
)
}
})
const limits: Options['limits'] = {
fieldNameSize,
fileSize
}
const fileFilter: Options['fileFilter'] = (
req: Request,
file: Express.Multer.File,
callback: FileFilterCallback
) => {
const fileExtension = path.extname(file.originalname)
const shouldBlockUpload = blockFileRegex.test(file.originalname)
if (shouldBlockUpload) {
return callback(
new Error(`File extension '${fileExtension}' not acceptable.`)
)
}
const uploadFileSize = parseInt(req.headers['content-length'] ?? '')
if (uploadFileSize > fileSize) {
return callback(
new Error(
`File size is over limit. File limit is: ${fileSize / 1024 / 1024} MB`
)
)
}
callback(null, true)
}
const options: Options = { storage, limits, fileFilter }
const multerInstance = multer(options)
export const multerSingle = (fileName: string, arg: any) => {
const [req, res, next] = arg
const upload = multerInstance.single(fileName)
upload(req, res, function (err) {
if (err instanceof multer.MulterError) {
return res.status(500).send(err.message)
} else if (err) {
return res.status(400).send(err.message)
}
// Everything went fine.
next()
})
}
export default multerInstance

View File

@@ -1,4 +1,6 @@
export const verifyAdmin = (req: any, res: any, next: any) => {
import { RequestHandler } from 'express'
export const verifyAdmin: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') return next()

View File

@@ -1,9 +1,22 @@
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
const { user } = req
const userId = parseInt(req.params.userId)
import { RequestHandler } from 'express'
if (!user.isAdmin && user.userId !== userId) {
return res.status(401).send('Admin account required')
// This middleware checks if a non-admin user trying to
// access information of other user
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
const { user } = req
if (!user?.isAdmin) {
let adminAccountRequired: boolean = true
if (req.params.userId) {
adminAccountRequired = user?.userId !== parseInt(req.params.userId)
} else if (req.params.username) {
adminAccountRequired = user?.username !== req.params.username
}
if (adminAccountRequired)
return res.status(401).send('Admin account required')
}
next()
}

View File

@@ -1,4 +1,5 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
import User from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose)
export interface GroupPayload {
@@ -34,7 +35,8 @@ interface IGroupModel extends Model<IGroup> {}
const groupSchema = new Schema<IGroupDocument>({
name: {
type: String,
required: true
required: true,
unique: true
},
description: {
type: String,
@@ -46,6 +48,7 @@ const groupSchema = new Schema<IGroupDocument>({
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks
@@ -55,6 +58,17 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
})
})
// pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function () {
const userIds = this.users
await Promise.all(
userIds.map(async (userId) => {
const user = await User.findById(userId)
user?.removeGroup(this._id)
})
)
})
// Instance Methods
groupSchema.method(
'addUser',

View File

@@ -27,18 +27,26 @@ export interface UserPayload {
* @example "true"
*/
isActive?: boolean
/**
* User-specific auto-exec code
* @example ""
*/
autoExec?: string
}
interface IUserDocument extends UserPayload, Document {
id: number
isAdmin: boolean
isActive: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
}
interface IUser extends IUserDocument {
comparePassword(password: string): boolean
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
}
interface IUserModel extends Model<IUser> {
hashPassword(password: string): string
@@ -66,6 +74,9 @@ const userSchema = new Schema<IUserDocument>({
type: Boolean,
default: true
},
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [
{
@@ -97,6 +108,28 @@ userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true
return false
})
userSchema.method(
'addGroup',
async function (groupObjectId: Schema.Types.ObjectId) {
const groupIdIndex = this.groups.indexOf(groupObjectId)
if (groupIdIndex === -1) {
this.groups.push(groupObjectId)
}
this.markModified('groups')
return this.save()
}
)
userSchema.method(
'removeGroup',
async function (groupObjectId: Schema.Types.ObjectId) {
const groupIdIndex = this.groups.indexOf(groupObjectId)
if (groupIdIndex > -1) {
this.groups.splice(groupIdIndex, 1)
}
this.markModified('groups')
return this.save()
}
)
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)

View File

@@ -1,21 +0,0 @@
import path from 'path'
import { readFileSync } from 'fs'
import * as https from 'https'
import appPromise from './app'
const keyPath = path.join('..', 'certificates', 'privkey.pem')
const certPath = path.join('..', 'certificates', 'fullchain.pem')
const key = readFileSync(keyPath)
const cert = readFileSync(certPath)
appPromise.then((app) => {
const httpsServer = https.createServer({ key, cert }, app)
const sasJsPort = process.env.PORT ?? 5000
httpsServer.listen(sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
)
})
})

View File

@@ -1,58 +1,22 @@
import express from 'express'
import { AuthController } from '../../controllers/'
import Client from '../../model/Client'
import {
authenticateAccessToken,
authenticateRefreshToken
} from '../../middlewares'
import {
authorizeValidation,
getDesktopFields,
tokenValidation
} from '../../utils'
import { authorizeValidation, tokenValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()
const clientIDs = new Set()
export const populateClients = async () => {
const result = await Client.find()
clientIDs.clear()
result.forEach((r) => {
clientIDs.add(r.clientId)
})
}
authRouter.post('/authorize', async (req, res) => {
const { error, value: body } = authorizeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const { clientId } = body
// Verify client ID
if (!clientIDs.has(clientId)) {
return res.status(403).send('Invalid clientId.')
}
const controller = new AuthController()
try {
const response = await controller.authorize(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
const controller = new AuthController()
authRouter.post('/token', async (req, res) => {
const { error, value: body } = tokenValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const controller = new AuthController()
try {
const response = await controller.token(body)
@@ -62,10 +26,12 @@ authRouter.post('/token', async (req, res) => {
}
})
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
const userInfo: InfoJWT = req.user
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
const controller = new AuthController()
try {
const response = await controller.refresh(userInfo)
@@ -75,10 +41,12 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
}
})
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
const userInfo: InfoJWT = req.user
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
const controller = new AuthController()
try {
await controller.logout(userInfo)
} catch (e) {}

View File

@@ -0,0 +1,31 @@
import express from 'express'
import { runCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/'
const runRouter = express.Router()
const controller = new CodeController()
runRouter.post('/execute', async (req, res) => {
const { error, value: body } = runCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeCode(req, body)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter

View File

@@ -1,13 +1,41 @@
import express from 'express'
import { deleteFile, readFile } from '@sasjs/utils'
import { publishAppStream } from '../appStream'
import { multerSingle } from '../../middlewares/multer'
import { DriveController } from '../../controllers/'
import { getFileDriveValidation, updateFileDriveValidation } from '../../utils'
import {
deployValidation,
extractJSONFromZip,
extractName,
fileBodyValidation,
fileParamValidation,
folderParamValidation,
isZipFile
} from '../../utils'
const controller = new DriveController()
const driveRouter = express.Router()
driveRouter.post('/deploy', async (req, res) => {
const controller = new DriveController()
const { error, value: body } = deployValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.deploy(req.body)
const response = await controller.deploy(body)
if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
@@ -18,59 +46,166 @@ driveRouter.post('/deploy', async (req, res) => {
}
})
driveRouter.post(
'/deploy/upload',
(...arg) => multerSingle('file', arg),
async (req, res) => {
if (!req.file) return res.status(400).send('"file" is not present.')
let fileContent: string = ''
const { value: zipFile } = isZipFile(req.file)
if (zipFile) {
fileContent = await extractJSONFromZip(zipFile)
const fileInZip = extractName(zipFile.originalname)
if (!fileContent) {
deleteFile(req.file.path)
return res
.status(400)
.send(
`No content present in ${fileInZip} of compressed file ${zipFile.originalname}`
)
}
} else {
fileContent = await readFile(req.file.path)
}
let jsonContent
try {
jsonContent = JSON.parse(fileContent)
} catch (err) {
deleteFile(req.file.path)
return res.status(400).send('File containing invalid JSON content.')
}
const { error, value: body } = deployValidation(jsonContent)
if (error) {
deleteFile(req.file.path)
return res.status(400).send(error.details[0].message)
}
try {
const response = await controller.deployUpload(req.file, body)
if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
} finally {
deleteFile(req.file.path)
}
}
)
driveRouter.get('/file', async (req, res) => {
const { error, value: query } = getFileDriveValidation(req.query)
if (error) return res.status(400).send(error.details[0].message)
const { error: errQ, value: query } = fileParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
const controller = new DriveController()
try {
const response = await controller.getFile(query.filePath)
res.send(response)
await controller.getFile(req, query._filePath)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
res.status(403).send(err.toString())
}
})
driveRouter.post('/file', async (req, res) => {
const { error, value: body } = updateFileDriveValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
driveRouter.get('/folder', async (req, res) => {
const { error: errQ, value: query } = folderParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
const controller = new DriveController()
try {
const response = await controller.saveFile(body)
const response = await controller.getFolder(query._folderPath)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
res.status(403).send(err.toString())
}
})
driveRouter.patch('/file', async (req, res) => {
const { error, value: body } = updateFileDriveValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
driveRouter.delete('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
const controller = new DriveController()
try {
const response = await controller.updateFile(body)
const response = await controller.deleteFile(query._filePath)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
res.status(403).send(err.toString())
}
})
driveRouter.post(
'/file',
(...arg) => multerSingle('file', arg),
async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) {
if (req.file) await deleteFile(req.file.path)
return res.status(400).send(errQ.details[0].message)
}
if (!req.file) return res.status(400).send('"file" is not present.')
try {
const response = await controller.saveFile(
req.file,
query._filePath,
body.filePath
)
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
res.status(403).send(err.toString())
}
}
)
driveRouter.patch(
'/file',
(...arg) => multerSingle('file', arg),
async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) {
if (req.file) await deleteFile(req.file.path)
return res.status(400).send(errQ.details[0].message)
}
if (!req.file) return res.status(400).send('"file" is not present.')
try {
const response = await controller.updateFile(
req.file,
query._filePath,
body.filePath
)
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
res.status(403).send(err.toString())
}
}
)
driveRouter.get('/fileTree', async (req, res) => {
const controller = new DriveController()
try {
const response = await controller.getFileTree()
res.send(response)

View File

@@ -1,7 +1,7 @@
import express from 'express'
import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import { registerGroupValidation } from '../../utils'
import { getGroupValidation, registerGroupValidation } from '../../utils'
const groupRouter = express.Router()
@@ -18,7 +18,11 @@ groupRouter.post(
const response = await controller.createGroup(body)
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)
}
}
)
@@ -29,35 +33,73 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
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)
}
})
groupRouter.get('/:groupId', authenticateAccessToken, async (req: any, res) => {
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const { groupId } = req.params
const controller = new GroupController()
try {
const response = await controller.getGroup(groupId)
const response = await controller.getGroup(parseInt(groupId))
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)
}
})
groupRouter.get(
'/by/groupname/:name',
authenticateAccessToken,
async (req, res) => {
const { error, value: params } = getGroupValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
const { name } = params
const controller = new GroupController()
try {
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)
}
}
)
groupRouter.post(
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req: any, res) => {
async (req, res) => {
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.addUserToGroup(groupId, userId)
const response = await controller.addUserToGroup(
parseInt(groupId),
parseInt(userId)
)
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)
}
}
)
@@ -66,15 +108,22 @@ groupRouter.delete(
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req: any, res) => {
async (req, res) => {
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.removeUserFromGroup(groupId, userId)
const response = await controller.removeUserFromGroup(
parseInt(groupId),
parseInt(userId)
)
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)
}
}
)
@@ -83,15 +132,19 @@ groupRouter.delete(
'/:groupId',
authenticateAccessToken,
verifyAdmin,
async (req: any, res) => {
async (req, res) => {
const { groupId } = req.params
const controller = new GroupController()
try {
await controller.deleteGroup(groupId)
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)

View File

@@ -8,15 +8,20 @@ import {
verifyAdmin
} from '../../middlewares'
import infoRouter from './info'
import driveRouter from './drive'
import stpRouter from './stp'
import codeRouter from './code'
import userRouter from './user'
import groupRouter from './group'
import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
const router = express.Router()
router.use('/info', infoRouter)
router.use('/session', authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter)
router.use(
'/client',
@@ -28,13 +33,24 @@ router.use(
router.use('/drive', authenticateAccessToken, driveRouter)
router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter)
router.use(
'/',
swaggerUi.serve,
swaggerUi.setup(undefined, {
swaggerOptions: {
url: '/swagger.yaml'
url: '/swagger.yaml',
requestInterceptor: (request: any) => {
request.credentials = 'include'
const cookie = document.cookie
const startIndex = cookie.indexOf('XSRF-TOKEN')
const csrf = cookie.slice(startIndex + 11).split('; ')[0]
request.headers['X-XSRF-TOKEN'] = csrf
return request
}
}
})
)

View File

@@ -0,0 +1,16 @@
import express from 'express'
import { InfoController } from '../../controllers'
const infoRouter = express.Router()
infoRouter.get('/', async (req, res) => {
const controller = new InfoController()
try {
const response = controller.info()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default infoRouter

View File

@@ -0,0 +1,16 @@
import express from 'express'
import { SessionController } from '../../controllers'
const sessionRouter = express.Router()
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())
}
})
export default sessionRouter

View File

@@ -8,7 +8,6 @@ import {
ClientController,
AuthController
} from '../../../controllers/'
import { populateClients } from '../auth'
import { InfoJWT } from '../../../types'
import {
generateAccessToken,
@@ -18,11 +17,6 @@ import {
verifyTokenInDB
} from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID'
const clientSecret = 'someclientSecret'
const user = {
@@ -35,16 +29,18 @@ const user = {
}
describe('auth', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
const clientController = new ClientController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
await clientController.createClient({ clientId, clientSecret })
await populateClients()
})
afterAll(async () => {
@@ -53,114 +49,6 @@ describe('auth', () => {
await mongoServer.stop()
})
describe('authorize', () => {
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with authorization code', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password,
clientId
})
.expect(200)
expect(res.body).toHaveProperty('code')
})
it('should respond with Bad Request if username is missing', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
password: user.password,
clientId
})
.expect(400)
expect(res.text).toEqual(`"username" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if password is missing', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
clientId
})
.expect(400)
expect(res.text).toEqual(`"password" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is incorrect', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password,
clientId
})
.expect(403)
expect(res.text).toEqual('Error: Username is not found.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if password is incorrect', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: 'WrongPassword',
clientId
})
.expect(403)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if clientId is incorrect', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password,
clientId: 'WrongClientID'
})
.expect(403)
expect(res.text).toEqual('Invalid clientId.')
expect(res.body).toEqual({})
})
})
describe('token', () => {
const userInfo: InfoJWT = {
clientId,

View File

@@ -6,11 +6,6 @@ import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const client = {
clientId: 'someclientID',
clientSecret: 'someclientSecret'
@@ -28,12 +23,15 @@ const newClient = {
}
describe('client', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
const clientController = new ClientController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
})

View File

@@ -1,20 +1,38 @@
import path from 'path'
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import AdmZip from 'adm-zip'
import {
folderExists,
fileExists,
readFile,
deleteFolder,
generateTimestamp,
copy,
createFolder,
createFile,
ServiceMember,
FolderMember
} from '@sasjs/utils'
import * as fileUtilModules from '../../../utils/file'
const timestamp = generateTimestamp()
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
jest
.spyOn(fileUtilModules, 'getSasjsRootFolder')
.mockImplementation(() => tmpFolder)
jest
.spyOn(fileUtilModules, 'getUploadsFolder')
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import { getTreeExample } from '../../../controllers/internal'
import { getTmpFilesFolderPath } from '../../../utils/file'
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
import path from 'path'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { FolderMember, ServiceMember } from '../../../types'
let app: Express
appPromise.then((_app) => {
app = _app
})
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getFilesFolder } = fileUtilModules
const clientId = 'someclientID'
const user = {
@@ -25,45 +43,94 @@ const user = {
isActive: true
}
describe('files', () => {
describe('drive', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const controller = new UserController()
let accessToken: string
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await controller.createUser(user)
accessToken = generateAccessToken({
clientId,
userId: dbUser.id
})
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
await deleteFolder(tmpFolder)
})
describe('deploy', () => {
let accessToken: string
let dbUser: any
beforeAll(async () => {
dbUser = await controller.createUser(user)
accessToken = generateAccessToken({
clientId,
userId: dbUser.id
})
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
})
const shouldFailAssertion = async (payload: any) => {
const res = await request(app)
.post('/SASjsApi/drive/deploy')
.auth(accessToken, { type: 'bearer' })
.send(payload)
describe('deploy', () => {
const makeRequest = async (payload: any, type: string = 'payload') => {
const requestUrl =
type === 'payload'
? '/SASjsApi/drive/deploy'
: '/SASjsApi/drive/deploy/upload'
if (type === 'payload') {
return await request(app)
.post(requestUrl)
.auth(accessToken, { type: 'bearer' })
.send({ appLoc: '/Public', fileTree: payload })
}
if (type === 'file') {
const deployContents = JSON.stringify({
appLoc: '/Public',
fileTree: payload
})
return await request(app)
.post(requestUrl)
.auth(accessToken, { type: 'bearer' })
.attach('file', Buffer.from(deployContents), 'deploy.json')
} else {
const deployContents = JSON.stringify({
appLoc: '/Public',
fileTree: payload
})
const zip = new AdmZip()
// add file directly
zip.addFile(
'deploy.json',
Buffer.from(deployContents, 'utf8'),
'entry comment goes here'
)
return await request(app)
.post(requestUrl)
.auth(accessToken, { type: 'bearer' })
.attach('file', zip.toBuffer(), 'deploy.json.zip')
}
}
const shouldFailAssertion = async (
payload: any,
type: string = 'payload'
) => {
const res = await makeRequest(payload, type)
expect(res.statusCode).toEqual(400)
expect(res.body).toEqual({
status: 'failure',
message: 'Provided not supported data format.',
example: getTreeExample()
})
if (payload === undefined) {
expect(res.text).toEqual('"fileTree" is required')
} else {
expect(res.body).toEqual({
status: 'failure',
message: 'Provided not supported data format.',
example: getTreeExample()
})
}
}
it('should respond with payload example if valid payload was not provided', async () => {
@@ -122,20 +189,21 @@ describe('files', () => {
})
})
it('should respond with payload example if valid payload was not provided', async () => {
it('should successfully deploy if valid payload was provided', async () => {
const res = await request(app)
.post('/SASjsApi/drive/deploy')
.auth(accessToken, { type: 'bearer' })
.send({ fileTree: getTreeExample() })
.send({ appLoc: '/public', fileTree: getTreeExample() })
expect(res.statusCode).toEqual(200)
expect(res.text).toEqual(
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
)
await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true)
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
const testJobFolder = path.join(
getTmpFilesFolderPath(),
getFilesFolder(),
'public',
'jobs',
'extract'
)
@@ -144,13 +212,732 @@ describe('files', () => {
const exampleService = getExampleService()
const testJobFile = path.join(testJobFolder, exampleService.name) + '.sas'
console.log(`[testJobFile]`, testJobFile)
await expect(fileExists(testJobFile)).resolves.toEqual(true)
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
await deleteFolder(getTmpFilesFolderPath())
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
describe('upload', () => {
it('should respond with payload example if valid JSON file was not provided', async () => {
await shouldFailAssertion(null, 'file')
await shouldFailAssertion(undefined, 'file')
await shouldFailAssertion('data', 'file')
await shouldFailAssertion({}, 'file')
await shouldFailAssertion(
{
userId: 1,
title: 'test is cool'
},
'file'
)
await shouldFailAssertion(
{
membersWRONG: []
},
'file'
)
await shouldFailAssertion(
{
members: {}
},
'file'
)
await shouldFailAssertion(
{
members: [
{
nameWRONG: 'jobs',
type: 'folder',
members: []
}
]
},
'file'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'WRONG',
members: []
}
]
},
'file'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'folder',
members: [
{
name: 'extract',
type: 'folder',
members: [
{
name: 'makedata1',
type: 'service',
codeWRONG: '%put Hello World!;'
}
]
}
]
}
]
},
'file'
)
})
it('should successfully deploy if valid JSON file was provided', async () => {
const deployContents = JSON.stringify({
appLoc: '/public',
fileTree: getTreeExample()
})
const res = await request(app)
.post('/SASjsApi/drive/deploy/upload')
.auth(accessToken, { type: 'bearer' })
.attach('file', Buffer.from(deployContents), 'deploy.json')
expect(res.statusCode).toEqual(200)
expect(res.text).toEqual(
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
)
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
const testJobFolder = path.join(
getFilesFolder(),
'public',
'jobs',
'extract'
)
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
const exampleService = getExampleService()
const testJobFile =
path.join(testJobFolder, exampleService.name) + '.sas'
await expect(fileExists(testJobFile)).resolves.toEqual(true)
await expect(readFile(testJobFile)).resolves.toEqual(
exampleService.code
)
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
})
describe('upload - zipped', () => {
it('should respond with payload example if valid Zipped file was not provided', async () => {
await shouldFailAssertion(null, 'zip')
await shouldFailAssertion(undefined, 'zip')
await shouldFailAssertion('data', 'zip')
await shouldFailAssertion({}, 'zip')
await shouldFailAssertion(
{
userId: 1,
title: 'test is cool'
},
'zip'
)
await shouldFailAssertion(
{
membersWRONG: []
},
'zip'
)
await shouldFailAssertion(
{
members: {}
},
'zip'
)
await shouldFailAssertion(
{
members: [
{
nameWRONG: 'jobs',
type: 'folder',
members: []
}
]
},
'zip'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'WRONG',
members: []
}
]
},
'zip'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'folder',
members: [
{
name: 'extract',
type: 'folder',
members: [
{
name: 'makedata1',
type: 'service',
codeWRONG: '%put Hello World!;'
}
]
}
]
}
]
},
'zip'
)
})
it('should successfully deploy if valid Zipped file was provided', async () => {
const deployContents = JSON.stringify({
appLoc: '/public',
fileTree: getTreeExample()
})
const zip = new AdmZip()
// add file directly
zip.addFile(
'deploy.json',
Buffer.from(deployContents, 'utf8'),
'entry comment goes here'
)
const res = await request(app)
.post('/SASjsApi/drive/deploy/upload')
.auth(accessToken, { type: 'bearer' })
.attach('file', zip.toBuffer(), 'deploy.json.zip')
expect(res.statusCode).toEqual(200)
expect(res.text).toEqual(
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
)
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
const testJobFolder = path.join(
getFilesFolder(),
'public',
'jobs',
'extract'
)
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
const exampleService = getExampleService()
const testJobFile =
path.join(testJobFolder, exampleService.name) + '.sas'
await expect(fileExists(testJobFile)).resolves.toEqual(true)
await expect(readFile(testJobFile)).resolves.toEqual(
exampleService.code
)
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
})
})
describe('folder', () => {
describe('get', () => {
const getFolderApi = '/SASjsApi/drive/folder'
it('should get root SAS folder on drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({ files: [], folders: [] })
})
it('should get a SAS folder on drive having _folderPath as query param', async () => {
const pathToDrive = fileUtilModules.getFilesFolder()
const dirLevel1 = 'level1'
const dirLevel2 = 'level2'
const fileLevel1 = 'file1'
const fileLevel2 = 'file2'
await createFolder(path.join(pathToDrive, dirLevel1, dirLevel2))
await createFile(
path.join(pathToDrive, dirLevel1, fileLevel1),
'some file content'
)
await createFile(
path.join(pathToDrive, dirLevel1, dirLevel2, fileLevel2),
'some file content'
)
const res1 = await request(app)
.get(getFolderApi)
.query({ _folderPath: '/' })
.auth(accessToken, { type: 'bearer' })
expect(res1.statusCode).toEqual(200)
expect(res1.body).toEqual({ files: [], folders: [dirLevel1] })
const res2 = await request(app)
.get(getFolderApi)
.query({ _folderPath: dirLevel1 })
.auth(accessToken, { type: 'bearer' })
expect(res2.statusCode).toEqual(200)
expect(res2.body).toEqual({ files: [fileLevel1], folders: [dirLevel2] })
const res3 = await request(app)
.get(getFolderApi)
.query({ _folderPath: `${dirLevel1}/${dirLevel2}` })
.auth(accessToken, { type: 'bearer' })
expect(res3.statusCode).toEqual(200)
expect(res3.body).toEqual({ files: [fileLevel2], folders: [] })
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get(getFolderApi).expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden 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(res.text).toEqual(`Error: Folder doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath outside Drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: '/../path/code.sas' })
.expect(403)
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath is of a file', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: filePath })
.expect(403)
expect(res.text).toEqual('Error: Not a Folder.')
expect(res.body).toEqual({})
})
})
})
describe('file', () => {
describe('create', () => {
it('should create a SAS file on drive having filePath as form field', async () => {
const pathToUpload = `/my/path/code-1.sas`
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should create a SAS file on drive having _filePath as query param', async () => {
const pathToUpload = `/my/path/code-2.sas`
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: pathToUpload })
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.post('/SASjsApi/drive/file')
.field('filePath', '/my/path/code.sas')
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is already present', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
const pathToCopy = path.join(
fileUtilModules.getFilesFolder(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
expect(res.text).toEqual('Error: File already exists.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
expect(res.text).toEqual('Error: Cannot put file outside drive.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if filePath is missing', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual(`"_filePath" is required`)
expect(res.body).toEqual({})
})
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.exe'
const res = await request(app)
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
.auth(accessToken, { type: 'bearer' })
// .field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if file is missing', async () => {
const pathToUpload = '/my/path/code.sas'
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.expect(400)
expect(res.text).toEqual('"file" is not present.')
expect(res.body).toEqual({})
})
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
const pathToUpload = '/my/path/code.sas'
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', attachedFile, 'another.sas')
.expect(400)
expect(res.text).toEqual(
'File size is over limit. File limit is: 100 MB'
)
expect(res.body).toEqual({})
})
})
describe('update', () => {
it('should update a SAS file on drive having filePath as form field', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getFilesFolder(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should update a SAS file on drive having _filePath as query param', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getFilesFolder(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.field('filePath', '/my/path/code.sas')
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden 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(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if filePath is missing', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual(`"_filePath" is required`)
expect(res.body).toEqual({})
})
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.exe'
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: pathToUpload })
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if file is missing', async () => {
const pathToUpload = '/my/path/code.sas'
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.expect(400)
expect(res.text).toEqual('"file" is not present.')
expect(res.body).toEqual({})
})
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
const pathToUpload = '/my/path/code.sas'
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', attachedFile, 'another.sas')
.expect(400)
expect(res.text).toEqual(
'File size is over limit. File limit is: 100 MB'
)
expect(res.body).toEqual({})
})
})
describe('get', () => {
it('should get a SAS file on drive having _filePath as query param', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const fileToCopyContent = await readFile(fileToCopyPath)
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: filePath })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({})
expect(res.text).toEqual(fileToCopyContent)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/drive/file').expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden 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(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden 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(res.text).toEqual('Error: Cannot get file outside drive.')
expect(res.body).toEqual({})
})
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/my/path/code.exe' })
.expect(400)
expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if filePath is missing', async () => {
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.expect(400)
expect(res.text).toEqual(`"_filePath" is required`)
expect(res.body).toEqual({})
})
})
})
})

View File

@@ -0,0 +1 @@
some code of sas

View File

@@ -0,0 +1 @@
some code of sas

View File

@@ -6,11 +6,6 @@ import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID'
const adminUser = {
displayName: 'Test Admin',
@@ -28,7 +23,7 @@ const user = {
}
const group = {
name: 'DCGroup1',
name: 'dcgroup1',
description: 'DC group for testing purposes.'
}
@@ -36,11 +31,14 @@ const userController = new UserController()
const groupController = new GroupController()
describe('group', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
@@ -72,6 +70,32 @@ describe('group', () => {
expect(res.body.users).toEqual([])
})
it('should respond with Conflict when group already exists with same name', async () => {
await groupController.createGroup(group)
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send(group)
.expect(409)
expect(res.text).toEqual('Group name already exists.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request when group name does not match the group name schema', async () => {
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...group, name: 'Wrong Group Name' })
.expect(400)
expect(res.text).toEqual(
'"name" must only contain alpha-numeric characters'
)
expect(res.body).toEqual({})
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).post('/SASjsApi/group').send().expect(401)
@@ -127,14 +151,51 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it(`should delete group's reference from users' groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser1 = await userController.createUser({
...user,
username: 'deletegroup1'
})
const dbUser2 = await userController.createUser({
...user,
username: 'deletegroup2'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res1 = await request(app)
.get(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res1.body.groups).toEqual([])
const res2 = await request(app)
.get(`/SASjsApi/user/${dbUser2.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res2.body.groups).toEqual([])
})
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.delete(`/SASjsApi/group/1234`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: No Group deleted!')
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
@@ -218,16 +279,76 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.get('/SASjsApi/group/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: Group not found.')
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
describe('by group name', () => {
it('should respond with group', async () => {
const { name } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/by/groupname/${name}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([])
})
it('should respond with group when access token is not of an admin account', async () => {
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'getbyname' + user.username
})
const { name } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/by/groupname/${name}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.get('/SASjsApi/group/by/groupname/dcgroup')
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Not Found if groupname is incorrect', async () => {
const res = await request(app)
.get('/SASjsApi/group/by/groupname/randomCharacters')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
})
})
describe('getAll', () => {
@@ -247,8 +368,8 @@ describe('group', () => {
expect(res.body).toEqual([
{
groupId: expect.anything(),
name: 'DCGroup1',
description: 'DC group for testing purposes.'
name: group.name,
description: group.description
}
])
})
@@ -269,8 +390,8 @@ describe('group', () => {
expect(res.body).toEqual([
{
groupId: expect.anything(),
name: 'DCGroup1',
description: 'DC group for testing purposes.'
name: group.name,
description: group.description
}
])
})
@@ -311,6 +432,34 @@ describe('group', () => {
])
})
it(`should add group to user's groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'addUserToGroup'
})
await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groups).toEqual([
{
groupId: expect.anything(),
name: group.name,
description: group.description
}
])
})
it('should respond with group without duplicating user', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
@@ -364,26 +513,26 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.post('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: Group not found.')
expect(res.text).toEqual('Group not found.')
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 () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User not found.')
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})
@@ -414,6 +563,29 @@ describe('group', () => {
expect(res.body.users).toEqual([])
})
it(`should remove group from user's groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'removeGroupFromUser'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groups).toEqual([])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
@@ -440,26 +612,26 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: Group not found.')
expect(res.text).toEqual('Group not found.')
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 () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User not found.')
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})

View File

@@ -0,0 +1,20 @@
import { Express } from 'express'
import request from 'supertest'
import appPromise from '../../../app'
describe('Info', () => {
let app: Express
beforeAll(async () => {
app = await appPromise
})
it('should should return configured information of the server instance', async () => {
const res = await request(app).get('/SASjsApi/info').expect(200)
expect(res.body.mode).toEqual('server')
expect(res.body.cors).toEqual('disable')
expect(res.body.whiteList).toEqual([])
expect(res.body.protocol).toEqual('http')
})
})

View File

@@ -0,0 +1,383 @@
import path from 'path'
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
getFilesFolder,
RunTimeType,
generateUniqueFileName,
getSessionsFolder
} from '../../../utils'
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types'
const clientId = 'someclientID'
const user = {
displayName: 'Test User',
username: 'testUsername',
password: '87654321',
isAdmin: false,
isActive: true
}
const sampleSasProgram = '%put hello world!;'
const sampleJsProgram = `console.log('hello world!/')`
const filesFolder = getFilesFolder()
describe('stp', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let accessToken: string
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
accessToken = await generateSaveTokenAndCreateUser(user)
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('execute', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
describe('get', () => {
describe('with runtime js', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
beforeAll(() => {
process.runTimes = [RunTimeType.JS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
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
)
})
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 request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
describe('with runtime sas', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.SAS]
})
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 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
)
})
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)
})
})
describe('with runtime js and sas', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.JS, RunTimeType.SAS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
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
)
})
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
)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
describe('with runtime sas and js', () => {
beforeAll(() => {
process.runTimes = [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 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
)
})
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
)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
})
})
})
const generateSaveTokenAndCreateUser = async (
someUser: any
): Promise<string> => {
const userController = new UserController()
const dbUser = await userController.createUser(someUser)
return generateAndSaveToken(dbUser.id)
}
const generateAndSaveToken = async (userId: number) => {
const accessToken = generateAccessToken({
clientId,
userId
})
await saveTokensInDB(userId, clientId, accessToken, 'refreshToken')
return accessToken
}
const setupMocks = async () => {
jest
.spyOn(SASSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(JSSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(() => Promise.resolve())
}
const mockedGetSession = async () => {
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,
ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
return session
}

View File

@@ -3,37 +3,36 @@ 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, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID'
const adminUser = {
displayName: 'Test Admin',
username: 'testAdminUsername',
username: 'testadminusername',
password: '12345678',
isAdmin: true,
isActive: true
}
const user = {
displayName: 'Test User',
username: 'testUsername',
username: 'testusername',
password: '87654321',
isAdmin: false,
isActive: true
isActive: true,
autoExec: 'some sas code for auto exec;'
}
const controller = new UserController()
describe('user', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
})
@@ -66,6 +65,21 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with new user having username as lowercase', async () => {
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...user, username: user.username.toUpperCase() })
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with Unauthorized if access token is not present', async () => {
@@ -244,7 +258,7 @@ describe('user', () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomUser'
username: 'randomuser'
})
const res = await request(app)
@@ -256,6 +270,102 @@ describe('user', () => {
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
describe('by username', () => {
it('should respond with updated user when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const newDisplayName = 'My new display Name'
const res = await request(app)
.patch(`/SASjsApi/user/by/username/${user.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName })
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(newDisplayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
})
it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
const res = await request(app)
.patch(`/SASjsApi/user/by/username/${user.username}`)
.auth(accessToken, { type: 'bearer' })
.send({
displayName: newDisplayName,
username: user.username,
password: user.password
})
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(newDisplayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
})
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
await request(app)
.patch(`/SASjsApi/user/by/username/${user.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName })
.expect(400)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/user/by/username/1234')
.send(user)
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomuser'
})
const res = await request(app)
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
})
})
describe('delete', () => {
@@ -349,6 +459,89 @@ describe('user', () => {
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
describe('by username', () => {
it('should respond with OK when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toEqual({})
})
it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: user.password })
.expect(200)
expect(res.body).toEqual({})
})
it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
expect(res.text).toEqual(`"password" is required`)
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/user/by/username/RandomUsername')
.send(user)
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
})
})
describe('get', () => {
@@ -362,7 +555,26 @@ describe('user', () => {
await deleteAllUsers()
})
it('should respond with user', async () => {
it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const res = await request(app)
.get(`/SASjsApi/user/${userId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
expect(res.body.groups).toEqual([])
})
it('should respond with user autoExec when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.id
@@ -376,6 +588,8 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
expect(res.body.groups).toEqual([])
})
it('should respond with user when access token is not of an admin account', async () => {
@@ -397,6 +611,35 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toBeUndefined()
expect(res.body.groups).toEqual([])
})
it('should respond with user along with associated groups', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const group = {
name: 'DCGroup1',
description: 'DC group for testing purposes.'
}
const groupController = new GroupController()
const dbGroup = await groupController.createGroup(group)
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app)
.get(`/SASjsApi/user/${userId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
expect(res.body.groups.length).toBeGreaterThan(0)
})
it('should respond with Unauthorized if access token is not present', async () => {
@@ -421,6 +664,86 @@ describe('user', () => {
expect(res.text).toEqual('Error: User is not found.')
expect(res.body).toEqual({})
})
describe('by username', () => {
it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const res = await request(app)
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with user autoExec when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const res = await request(app)
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with user when access token is not of an admin account', async () => {
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'randomUser'
})
const dbUser = await controller.createUser(user)
const res = await request(app)
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toBeUndefined()
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.get('/SASjsApi/user/by/username/randomUsername')
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden 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(res.text).toEqual('Error: User is not found.')
expect(res.body).toEqual({})
})
})
})
describe('getAll', () => {

View File

@@ -0,0 +1,182 @@
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
const clientId = 'someclientID'
const clientSecret = 'someclientSecret'
const user = {
id: 1234,
displayName: 'Test User',
username: 'testusername',
password: '87654321',
isAdmin: false,
isActive: true
}
describe('web', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
const clientController = new ClientController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
await clientController.createClient({ clientId, clientSecret })
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
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=\//
)
})
})
describe('SASLogon/login', () => {
let csrfToken: string
let cookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with successful login', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: user.password
})
.expect(200)
expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({
id: expect.any(Number),
username: user.username,
displayName: user.displayName
})
})
})
describe('SASLogon/authorize', () => {
let csrfToken: string
let cookies: string
let authCookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
await userController.createUser(user)
const credentials = {
username: user.username,
password: user.password
}
;({ cookies: authCookies } = await performLogin(
app,
credentials,
cookies,
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, cookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({ clientId })
expect(res.body).toHaveProperty('code')
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].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, cookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
})
.expect(403)
expect(res.text).toEqual('Error: Invalid clientId.')
expect(res.body).toEqual({})
})
})
})
const getCSRF = async (app: Express) => {
// make request to get CSRF
const { header } = await request(app).get('/')
const cookies = header['set-cookie'].join()
const csrfToken = extractCSRF(cookies)
return { csrfToken, cookies }
}
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 }
}
const extractCSRF = (cookies: string) =>
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
cookies
)![2]

View File

@@ -14,6 +14,12 @@ stpRouter.get('/execute', async (req, res) => {
try {
const response = await controller.executeReturnRaw(req, query._program)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
@@ -26,20 +32,28 @@ stpRouter.get('/execute', async (req, res) => {
stpRouter.post(
'/execute',
fileUploadController.preuploadMiddleware,
fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(),
async (req: any, res: any) => {
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
const { error: errB, value: body } = executeProgramRawValidation(req.body)
async (req, res: any) => {
// below validations are moved to preUploadMiddleware
// const { error: errQ, value: query } = executeProgramRawValidation(req.query)
// const { error: errB, value: body } = executeProgramRawValidation(req.body)
if (errQ && errB) return res.status(400).send(errB.details[0].message)
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
try {
const response = await controller.executeReturnJson(
req,
body,
query?._program
req.body,
req.query?._program as string
)
// TODO: investigate if this code is required
// if (response instanceof Buffer) {
// res.writeHead(200, (req as any).sasHeaders)
// return res.end(response)
// }
res.send(response)
} catch (err: any) {
const statusCode = err.code

View File

@@ -7,6 +7,7 @@ import {
} from '../../middlewares'
import {
deleteUserValidation,
getUserValidation,
registerUserValidation,
updateUserValidation
} from '../../utils'
@@ -36,12 +37,31 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
}
})
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
userRouter.get(
'/by/username/:username',
authenticateAccessToken,
async (req, res) => {
const { error, value: params } = getUserValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
const { username } = params
const controller = new UserController()
try {
const response = await controller.getUserByUsername(req, username)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const { userId } = req.params
const controller = new UserController()
try {
const response = await controller.getUser(userId)
const response = await controller.getUser(req, parseInt(userId))
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
@@ -49,20 +69,26 @@ userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
})
userRouter.patch(
'/:userId',
'/by/username/:username',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req: any, res) => {
async (req, res) => {
const { user } = req
const { userId } = req.params
const { error: errorUsername, value: params } = getUserValidation(
req.params
)
if (errorUsername)
return res.status(400).send(errorUsername.details[0].message)
const { username } = params
// only an admin can update `isActive` and `isAdmin` fields
const { error, value: body } = updateUserValidation(req.body, user.isAdmin)
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
const response = await controller.updateUser(userId, body)
const response = await controller.updateUserByUsername(username, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
@@ -70,21 +96,71 @@ userRouter.patch(
}
)
userRouter.patch(
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req, res) => {
const { user } = req
const { userId } = req.params
// only an admin can update `isActive` and `isAdmin` fields
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
const response = await controller.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
userRouter.delete(
'/by/username/:username',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req, res) => {
const { user } = req
const { error: errorUsername, value: params } = getUserValidation(
req.params
)
if (errorUsername)
return res.status(400).send(errorUsername.details[0].message)
const { username } = params
// only an admin can delete user without providing password
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
await controller.deleteUserByUsername(username, data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
userRouter.delete(
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req: any, res) => {
async (req, res) => {
const { user } = req
const { userId } = req.params
// only an admin can delete user without providing password
const { error, value: data } = deleteUserValidation(req.body, user.isAdmin)
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
await controller.deleteUser(userId, data, user.isAdmin)
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())

View File

@@ -0,0 +1,52 @@
import { AppStreamConfig } from '../../types'
import { style } from './style'
const defaultAppLogo = '/sasjs-logo.svg'
const singleAppStreamHtml = (
streamServiceName: string,
appLoc: string,
logo?: string
) =>
` <a class="app" href="${streamServiceName}" title="${appLoc}">
<img
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
onerror="this.src = '${defaultAppLogo}';"
/>
${streamServiceName}
</a>`
export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
<html>
<head>
<base href="/AppStream/">
${style}
</head>
<body>
<header>
<a href="/"><img src="/logo.png" alt="logo" class="logo"></a>
<h1>App Stream</h1>
</header>
<div class="app-container">
${Object.entries(appStreamConfig)
.map(([streamServiceName, entry]) =>
singleAppStreamHtml(
streamServiceName,
entry.appLoc,
entry.streamLogo
)
)
.join('')}
<a class="app" title="Upload build.json">
<input id="fileId" type="file" hidden />
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
<img src="/plus.png" />
</button>
<span id="uploadMessage">Upload New App</span>
</a>
</div>
<script src="/axios.min.js"></script>
<script src="/app-streams-script.js"></script>
</body>
</html>`

View File

@@ -0,0 +1,91 @@
import path from 'path'
import express, { Request } from 'express'
import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
import { appStreamHtml } from './appStreamHtml'
const appStreams: { [key: string]: string } = {}
const router = express.Router()
router.get('/', async (req, res) => {
const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken())
return res.send(content)
})
export const publishAppStream = async (
appLoc: string,
streamWebFolder: string,
streamServiceName?: string,
streamLogo?: string,
addEntryToFile: boolean = true
) => {
const driveFilesPath = getFilesFolder()
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
const appLocPath = path.join(driveFilesPath, ...appLocParts)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
const pathToDeployment = path.join(appLocPath, 'services', streamWebFolder)
if (!pathToDeployment.includes(appLocPath)) {
throw new Error('streamWebFolder cannot be outside appLoc.')
}
if (await folderExists(pathToDeployment)) {
const appCount = process.appStreamConfig
? Object.keys(process.appStreamConfig).length
: 0
if (!streamServiceName) {
streamServiceName = `AppStreamName${appCount + 1}`
}
appStreams[streamServiceName] = pathToDeployment
addEntryToAppStreamConfig(
streamServiceName,
appLoc,
streamWebFolder,
streamLogo,
addEntryToFile
)
const sasJsPort = process.env.PORT || 5000
console.log(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)
return { streamServiceName }
}
return {}
}
router.get(`/*`, function (req: Request, res, next) {
const reqPath = req.path.replace(/^\//, '')
// Redirecting to url with trailing slash for appStream base URL only
if (reqPath.split('/').length === 1 && !reqPath.endsWith('/'))
// navigating to same url with slash at start
return res.redirect(301, `${reqPath}/`)
const appStream = reqPath.split('/')[0]
const appStreamFilesPath = appStreams[appStream]
if (appStreamFilesPath) {
// resourcePath is without appStream base path
const resourcePath = reqPath.split('/').slice(1).join('/') || 'index.html'
req.url = resourcePath
return express.static(appStreamFilesPath)(req, res, next)
}
return res.send("There's no App Stream available here.")
})
export default router

View File

@@ -0,0 +1,75 @@
export const style = `<style>
* {
font-family: 'Roboto', sans-serif;
}
.app-container {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
padding-top: 50px;
}
.app-container .app {
width: 150px;
height: 180px;
margin: 10px;
overflow: hidden;
text-align: center;
text-decoration: none;
color: black;
background: #efefef;
padding: 10px;
border-radius: 7px;
border: 1px solid #d7d7d7;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
border-radius: 10px;
}
#uploadButton {
border: 0
}
#uploadButton:focus {
outline: 0
}
#uploadMessage {
position: relative;
bottom: -5px;
}
header {
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
box-shadow: rgb(0 0 0 / 20%) 0px 2px 4px -1px, rgb(0 0 0 / 14%) 0px 4px 5px 0px, rgb(0 0 0 / 12%) 0px 1px 10px 0px;
display: flex;
width: 100%;
box-sizing: border-box;
flex-shrink: 0;
position: fixed;
top: 0px;
left: auto;
right: 0px;
background-color: rgb(0, 0, 0);
color: rgb(255, 255, 255);
z-index: 1201;
}
header h1 {
margin: 13px;
font-size: 20px;
}
header a {
align-self: center;
}
header .logo {
width: 35px;
margin-left: 10px;
align-self: center;
}
</style>`

View File

@@ -0,0 +1,19 @@
import { Express } from 'express'
import webRouter from './web'
import apiRouter from './api'
import appStreamRouter from './appStream'
import { csrfProtection } from '../app'
export const setupRoutes = (app: Express) => {
app.use('/SASjsApi', apiRouter)
app.use('/AppStream', csrfProtection, function (req, res, next) {
// this needs to be a function to hook on
// whatever the current router is
appStreamRouter(req, res, next)
})
app.use('/', csrfProtection, webRouter)
}

View File

@@ -1,34 +1,60 @@
import { readFile } from '@sasjs/utils'
import express from 'express'
import path from 'path'
import { getWebBuildFolderPath } from '../../utils'
import { WebController } from '../../controllers/web'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils'
const webRouter = express.Router()
const controller = new WebController()
const codeToInject = `
<script>
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
</script>`
webRouter.get('/', async (_, res) => {
let content: string
webRouter.get('/', async (req, res) => {
let response
try {
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
content = await readFile(indexHtmlPath)
response = await controller.home()
} catch (_) {
return res.send('Web Build is not present')
response = 'Web Build is not present'
} finally {
res.cookie('XSRF-TOKEN', req.csrfToken())
return res.send(response)
}
})
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
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)
res.setHeader('Content-Type', 'text/html')
return res.send(injectedContent)
try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
return res.send(content)
webRouter.post(
'/SASLogon/authorize',
desktopRestrict,
authenticateAccessToken,
async (req, res) => {
const { error, value: body } = authorizeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.authorize(req, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
webRouter.get('/logout', desktopRestrict, async (req, res) => {
try {
await controller.logout(req)
res.status(200).send('OK!')
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default webRouter

View File

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

View File

@@ -0,0 +1,7 @@
export interface AppStreamConfig {
[key: string]: {
appLoc: string
streamWebFolder: string
streamLogo?: string
}
}

View File

@@ -1,47 +0,0 @@
export interface FileTree {
members: (FolderMember | ServiceMember)[]
}
export enum MemberType {
folder = 'folder',
service = 'service'
}
export interface FolderMember {
name: string
type: MemberType.folder
members: (FolderMember | ServiceMember)[]
}
export interface ServiceMember {
name: string
type: MemberType.service
code: string
}
export const isFileTree = (arg: any): arg is FileTree =>
arg &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isFolderMember = (arg: any): arg is FolderMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.folder &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isServiceMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
arg.code &&
typeof arg.code === 'string'

View File

@@ -3,5 +3,5 @@ export interface PreProgramVars {
userId: number
displayName: string
serverUrl: string
accessToken: string
httpHeaders: string[]
}

View File

@@ -1,7 +0,0 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
driveLoc?: string
sessionController?: import('../controllers/internal').SessionController
}
}

View File

@@ -1,17 +0,0 @@
import { MacroVars } from '@sasjs/utils'
export interface ExecutionQuery {
_program: string
macroVars?: MacroVars
_debug?: number
}
export interface FileQuery {
filePath: string
}
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
arg && !Array.isArray(arg) && typeof arg._program === 'string'
export const isFileQuery = (arg: any): arg is FileQuery =>
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'

View File

@@ -0,0 +1,9 @@
export interface RequestUser {
userId: number
clientId: string
username: string
displayName: string
isAdmin: boolean
isActive: boolean
autoExec?: string
}

View File

@@ -5,6 +5,7 @@ export interface Session {
deathTimeStamp: string
path: string
inUse: boolean
consumed: boolean
completed: boolean
crashed?: string
}

View File

@@ -1,8 +1,8 @@
// TODO: uppercase types
export * from './AppStreamConfig'
export * from './Execution'
export * from './FileTree'
export * from './InfoJWT'
export * from './PreProgramVars'
export * from './Request'
export * from './Session'
export * from './TreeNode'
export * from './RequestUser'

View File

@@ -0,0 +1,7 @@
import express from 'express'
declare module 'express-session' {
interface SessionData {
loggedIn: boolean
user: import('../').RequestUser
}
}

7
api/src/types/system/express.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare namespace Express {
export interface Request {
accessToken?: string
user?: import('../').RequestUser
sasjsSession?: import('../').Session
}
}

1
api/src/types/system/global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import 'jest-extended'

12
api/src/types/system/process.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
nodeLoc: string
driveLoc: string
sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]
}
}

View File

@@ -0,0 +1,89 @@
import { createFile, fileExists, readFile } from '@sasjs/utils'
import { publishAppStream } from '../routes/appStream'
import { AppStreamConfig } from '../types'
import { getAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
if (process.env.NODE_ENV === 'test') return
const appStreamConfigPath = getAppStreamConfigPath()
const content = (await fileExists(appStreamConfigPath))
? await readFile(appStreamConfigPath)
: '{}'
let appStreamConfig: AppStreamConfig
try {
appStreamConfig = JSON.parse(content)
if (!isValidAppStreamConfig(appStreamConfig)) throw 'invalid type'
} catch (_) {
appStreamConfig = {}
}
process.appStreamConfig = {}
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
const { appLoc, streamWebFolder, streamLogo } = entry
publishAppStream(
appLoc,
streamWebFolder,
streamServiceName,
streamLogo,
false
)
}
console.log('App Stream Config loaded!')
}
export const addEntryToAppStreamConfig = (
streamServiceName: string,
appLoc: string,
streamWebFolder: string,
streamLogo?: string,
addEntryToFile: boolean = true
) => {
if (streamServiceName && appLoc && streamWebFolder) {
process.appStreamConfig[streamServiceName] = {
appLoc,
streamWebFolder,
streamLogo
}
if (addEntryToFile) saveAppStreamConfig()
}
}
export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
if (streamServiceName) {
delete process.appStreamConfig[streamServiceName]
saveAppStreamConfig()
}
}
const saveAppStreamConfig = async () => {
const appStreamConfigPath = getAppStreamConfigPath()
try {
await createFile(
appStreamConfigPath,
JSON.stringify(process.appStreamConfig, null, 2)
)
} catch (_) {}
}
const isValidAppStreamConfig = (config: any) => {
if (config) {
return !Object.entries(config).some(([streamServiceName, entry]) => {
const { appLoc, streamWebFolder, streamLogo } = entry as any
return (
typeof streamServiceName !== 'string' ||
typeof appLoc !== 'string' ||
typeof streamWebFolder !== 'string'
)
})
}
return false
}

View File

@@ -1,38 +1,15 @@
import path from 'path'
import mongoose from 'mongoose'
import { configuration } from '../../package.json'
import { getDesktopFields } from '.'
import { populateClients } from '../routes/api/auth'
import { seedDB } from './seedDB'
export const connectDB = async () => {
// NOTE: when exporting app.js as agent for supertest
// we should exlcude connecting to the real database
if (process.env.NODE_ENV !== 'test') {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
const { sasLoc, driveLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.driveLoc = driveLoc
return
} else {
const { SAS_PATH } = process.env
const sasDir = SAS_PATH ?? configuration.sasPath
process.sasLoc = path.join(sasDir, 'sas')
}
console.log('sasLoc: ', process.sasLoc)
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err
console.log('Connected to db!')
await populateClients()
})
try {
await mongoose.connect(process.env.DB_CONNECT as string)
} catch (err) {
throw new Error('Unable to connect to DB!')
}
console.log('Connected to DB!')
await seedDB()
return mongoose.connection
}

View File

@@ -0,0 +1,34 @@
import path from 'path'
import {
asyncForEach,
createFile,
createFolder,
deleteFolder,
readFile
} from '@sasjs/utils'
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).')
const macrosDrivePath = getMacrosFolder()
await deleteFolder(macrosDrivePath)
await createFolder(macrosDrivePath)
const macros = await readFile(sasJSCoreMacrosInfo)
await asyncForEach(macros.split('\n'), async (macroName) => {
const macroFileSourcePath = path.join(sasJSCoreMacros, macroName)
const macroContent = await readFile(macroFileSourcePath)
const macroFileDestPath = path.join(macrosDrivePath, macroName)
await createFile(macroFileDestPath, macroContent)
})
console.log('Macros Drive Path:', macrosDrivePath)
}

View File

@@ -0,0 +1,8 @@
import { createFile, readFile } from '@sasjs/utils'
import { getDesktopUserAutoExecPath } from './file'
export const getUserAutoExec = async (): Promise<string> =>
readFile(getDesktopUserAutoExecPath())
export const updateUserAutoExec = async (autoExecContent: string) =>
createFile(getDesktopUserAutoExecPath(), autoExecContent)

View File

@@ -0,0 +1,25 @@
const headerUtils = require('http-headers-validation')
export interface HTTPHeaders {
[key: string]: string
}
export const extractHeaders = (content?: string): HTTPHeaders => {
const headersObj: HTTPHeaders = {}
const headersArr = content
?.split('\n')
.map((line) => line.trim())
.filter((line) => !!line)
headersArr?.forEach((headerStr) => {
const [key, value] = headerStr.split(':').map((data) => data.trim())
if (value && headerUtils.validateHeader(key, value)) {
headersObj[key.toLowerCase()] = value
} else {
delete headersObj[key.toLowerCase()]
}
})
return headersObj
}

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