1
0
mirror of https://github.com/sasjs/server.git synced 2026-07-23 21:25:29 +00:00

Compare commits

...

339 Commits

Author SHA1 Message Date
semantic-release-bot 9c3a1086b6 chore(release): 0.39.7 [skip ci]
## [0.39.7](https://github.com/sasjs/server/compare/v0.39.6...v0.39.7) (2026-07-14)

### Bug Fixes

* **jsonwebtoken:** bumped version to avoid vulnerability ([1473925](https://github.com/sasjs/server/commit/1473925896db0e4472c9ef5ba64955527a1be2de))
2026-07-14 13:53:13 +00:00
Yury Shkoda 05768890a2 Merge pull request #366 from sasjs/cpr-fix
Fix API scripts
2026-07-14 16:50:34 +03:00
YuryShkoda 386185d1a0 Merge remote-tracking branch 'origin/main' into cpr-fix
# Conflicts:
#	api/package-lock.json
#	api/package.json
#	api/public/swagger.yaml
2026-07-14 16:28:10 +03:00
Yury Shkoda 681f0123ad Merge pull request #387 from sasjs/fix/pin-axios-version
chore: pin axios versions
2026-07-14 15:45:40 +03:00
YuryShkoda daa5274fb0 Merge remote-tracking branch 'origin/main' into fix/pin-axios-version 2026-07-14 15:29:26 +03:00
semantic-release-bot 50fa4320cd chore(release): 0.39.6 [skip ci]
## [0.39.6](https://github.com/sasjs/server/compare/v0.39.5...v0.39.6) (2026-07-14)

### Bug Fixes

* **api:** isolate drive.spec.ts's files folder from the real drive ([dec5191](https://github.com/sasjs/server/commit/dec51914911c10b66b43ebe95f77b0af63e2d03e))
* **api:** stop overwriting a failed JS/PY/R session back to completed ([4858245](https://github.com/sasjs/server/commit/4858245372fa19929b6e59c28148f5d004239c9d))
2026-07-14 09:05:49 +00:00
Allan Bowe 40b9fa8735 Merge pull request #391 from sasjs/issue-390
Issue 390
2026-07-14 10:02:31 +01:00
YuryShkoda fd31fe94ea Merge remote-tracking branch 'origin/main' into issue-390
# Conflicts:
#	api/src/controllers/internal/spec/Execution.spec.ts
2026-07-14 11:58:24 +03:00
YuryShkoda dec5191491 fix(api): isolate drive.spec.ts's files folder from the real drive
drive.spec.ts already isolates itself into a unique, timestamped
tmpFolder for getSasjsRootFolder()/getUploadsFolder(), cleaned up via
afterAll(() => deleteFolder(tmpFolder)). But getFilesFolder() - what
nearly every test in this file actually writes to - resolves through
a separate, unmocked function (getSasjsDriveFolder()/process.driveLoc),
so every run left real files/folders (e.g. 'level1', 'my/path/...')
behind in the shared api/sasjs_root/drive/files, causing later runs
to fail: folder-listing tests saw stale entries, and file-creation
tests got 409 Conflict against files a previous run already created.

Mock getFilesFolder() the same way getUploadsFolder() already is, so
it resolves inside the same isolated tmpFolder and gets cleaned up
by the existing afterAll. Verified by running the suite twice in a
row from a clean slate: both runs pass, and the real sasjs_root/drive
is never touched.
2026-07-14 11:32:01 +03:00
YuryShkoda 4858245372 fix(api): stop overwriting a failed JS/PY/R session back to completed
For JS/PY/R, processProgram sets session.state = failed (with
failureReason) itself when the interpreter process exits non-zero,
without throwing. ExecutionController.executeProgram then
unconditionally set session.state = completed right after
processProgram returned, silently overwriting that - so anything
downstream inspecting session.state (e.g. scheduleSessionDestroy's
expiresAfterMins branch) would see a crashed session mis-reported as
successful.

Guard the assignment so a failed state is never overwritten. No
effect on SAS, which sets state independently via its own spawned
process lifecycle.

Added Execution.spec.ts covering both the failure case (state stays
failed) and the success case (state still becomes completed), to
guard against regressing in either direction.
2026-07-14 11:29:30 +03:00
semantic-release-bot 2fe733d02f chore(release): 0.39.5 [skip ci]
## [0.39.5](https://github.com/sasjs/server/compare/v0.39.4...v0.39.5) (2026-07-14)

### Bug Fixes

* **api:** harden mock SAS executable and timeouts for CI ([67fce47](https://github.com/sasjs/server/commit/67fce475a3c53b3c20919902edb45dce809dffb9))
* **api:** return prompt error response instead of hanging when a SAS session fails ([f84af4a](https://github.com/sasjs/server/commit/f84af4ac0639625fee4508e764f0eed07d7b4e8a))
* fix code.spec.ts failing on a fresh checkout (CI) ([0af6b63](https://github.com/sasjs/server/commit/0af6b63ffa770bfa2b77505d74778be517f8d631))
2026-07-14 06:38:40 +00:00
Allan Bowe 63086a8a0b Merge pull request #389 from sasjs/issue-388
fix(api): return prompt error response instead of hanging when a SAS …
2026-07-14 07:35:39 +01:00
YuryShkoda 0af6b63ffa fix: fix code.spec.ts failing on a fresh checkout (CI) 2026-07-14 09:20:35 +03:00
YuryShkoda 67fce475a3 fix(api): harden mock SAS executable and timeouts for CI
- wrap the file read/write in a small retry() helper instead of
  letting an uncaught exception (e.g. a rename racing an existsSync
  check) crash the process with a non-zero exit indistinguishable
  from a genuine SAS failure
- bump the internal give-up deadline from 1500ms to 8000ms
- bump the corresponding Jest test timeouts from 15000ms to 30000ms to match
2026-07-14 08:41:35 +03:00
YuryShkoda f84af4ac06 fix(api): return prompt error response instead of hanging when a SAS session fails
The SAS-runtime poll loop in processProgram only checked for
SessionState.completed, so a session that failed (e.g. via %abort;)
left the loop spinning on delay(50) forever - the request never
resolved, even though the SAS log had already been written. Split a single completed-on-either-outcome
flag into separate completed/failed states without updating this loop.

- processProgram now throws when session.state becomes 'failed'
- Execution.ts catches that, reads the complete log (guaranteed
  complete since the session's process has already exited by then),
  and throws a SessionExecutionError carrying it
- stp.ts/code.ts surface { ..., log } in the HTTP error response
- add unit tests for the poll loop and the log-enrichment logic, plus
  a mock SAS executable + end-to-end test exercising the real
  session/process pipeline without needing a SAS install
- add docs/diagrams covering the session lifecycle and the SAS
  execution handshake mechanism, for future context
2026-07-13 13:46:36 +03:00
mulahasanovic 4f75fd290f chore: pin axios versions 2026-04-01 20:55:50 +02:00
semantic-release-bot 8b2a6155a9 chore(release): 0.39.4 [skip ci]
## [0.39.4](https://github.com/sasjs/server/compare/v0.39.3...v0.39.4) (2025-12-21)

### Bug Fixes

* **deps:** bump dependencies and resolve audit issues ([4f78202](https://github.com/sasjs/server/commit/4f782025dbcdfcbae6ca1fabb42ce1bc385e0162))
2025-12-21 01:27:37 +00:00
Trevor Moody a56c0b0340 Merge pull request #386 from sasjs/depBumps_20251221
fix(deps): bump dependencies and resolve audit issues
2025-12-21 01:24:31 +00:00
Trevor Moody 02fe79d4d7 chore(deps): full package-lock rebuild 2025-12-21 01:22:35 +00:00
Trevor Moody 00a107babd chore(deps): rebuilt api/package-json to sync 2025-12-21 01:15:05 +00:00
Trevor Moody 4f782025db fix(deps): bump dependencies and resolve audit issues 2025-12-21 00:27:24 +00:00
semantic-release-bot 8b5abcd661 chore(release): 0.39.3 [skip ci]
## [0.39.3](https://github.com/sasjs/server/compare/v0.39.2...v0.39.3) (2025-11-25)

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

* **api:** fixed condition in processProgram ([48a9a4d](https://github.com/sasjs/server/commit/48a9a4dd0e31f84209635382be4ec4bb2c3a9c0c))

### Features

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

### Features

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

### Features

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

### Features

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

### Bug Fixes

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

### Bug Fixes

* enable embedded LFs in JS STP vars ([7e8cbbf](https://github.com/sasjs/server/commit/7e8cbbf377b27a7f5dd9af0bc6605c01f302f5d9))
2023-11-07 20:48:28 +00:00
Allan Bowe 28222add04 Merge pull request #370 from sasjs/allanbowe-patch-1
fix: enable embedded LFs in JS STP vars
2023-11-07 20:43:16 +00:00
Allan 068edfd6a5 chore: lint fix 2023-11-07 20:39:05 +00:00
Allan Bowe 7e8cbbf377 fix: enable embedded LFs in JS STP vars 2023-11-07 15:51:32 +00:00
Yury Shkoda 66232aefd2 chore(lint): bumped prettier and fixed lint issues 2023-09-21 17:58:47 +03:00
Yury Shkoda bf35791655 chore(lint): fixed Session.ts 2023-09-21 17:52:28 +03:00
Yury Shkoda 2dc11630e4 chore(api): regenerated swagger.yaml 2023-09-21 17:45:52 +03:00
Yury Shkoda 1473925896 fix(jsonwebtoken): bumped version to avoid vulnerability 2023-09-21 17:45:21 +03:00
Yury Shkoda b472f1bd61 chore(scripts): used cpr package to improve coping 2023-09-21 17:44:31 +03:00
Allan Bowe 1fc1431442 chore: using GITHUB_TOKEN 2023-08-07 20:11:40 +01:00
semantic-release-bot 3387efbb9a chore(release): 0.35.2 [skip ci]
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)

### Bug Fixes

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

### Bug Fixes

* **log-separator:** log separator should always wrap log ([8940f4d](https://github.com/sasjs/server/commit/8940f4dc47abae2036b4fcdeb772c31a0ca07cca))
2023-07-25 06:05:23 +00:00
Yury Shkoda 2f47a2213b Merge pull request #364 from sasjs/log-separator
fix(log-separator): log separator should always wrap log
2023-07-25 09:01:36 +03:00
Yury Shkoda 0f91395fbb lint: fixed linting issues 2023-07-24 18:36:08 +03:00
Yury Shkoda 167b14fed0 docs(log-separator): left comment 2023-07-24 18:29:20 +03:00
Yury Shkoda 8940f4dc47 fix(log-separator): log separator should always wrap log 2023-07-24 18:27:21 +03:00
semantic-release-bot 48c1ada1b6 chore(release): 0.35.0 [skip ci]
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)

### Bug Fixes

* **editor:** fixed log/webout/print tabs ([d2de9dc](https://github.com/sasjs/server/commit/d2de9dc13ef2e980286dd03cca5e22cea443ed0c))
* **execute:** added atribute indicating stp api ([e78f87f](https://github.com/sasjs/server/commit/e78f87f5c00038ea11261dffb525ac8f1024e40b))
* **execute:** fixed adding print output ([9aaffce](https://github.com/sasjs/server/commit/9aaffce82051d81bf39adb69942bb321e9795141))
* **execution:** removed empty webout from response ([6dd2f4f](https://github.com/sasjs/server/commit/6dd2f4f87673336135bc7a6de0d2e143e192c025))
* **webout:** fixed adding empty webout to response payload ([31df72a](https://github.com/sasjs/server/commit/31df72ad88fe2c771d0ef8445d6db9dd147c40c9))

### Features

* **editor:** parse print output in response payload ([eb42683](https://github.com/sasjs/server/commit/eb42683fff701bd5b4d2b68760fe0c3ecad573dd))
2023-05-03 09:34:56 +00:00
Allan Bowe 0532488b55 Merge pull request #360 from sasjs/issue-354
Support print destination natively
2023-05-03 10:31:06 +01:00
Yury Shkoda d458b5bb81 chore: cleanup 2023-05-03 10:56:17 +03:00
Yury Shkoda 958ab9cad2 chore(execution): add includePrintOutput to ExecuteFileParams 2023-05-03 10:46:21 +03:00
Yury Shkoda 78ceed13e1 docs(code): updated execute endpoint info 2023-05-02 16:01:00 +03:00
Yury Shkoda a17814fc90 chore(stp): removed redundant argument 2023-05-02 15:53:13 +03:00
Yury Shkoda 9aaffce820 fix(execute): fixed adding print output 2023-05-02 15:49:44 +03:00
Yury Shkoda e78f87f5c0 fix(execute): added atribute indicating stp api 2023-05-02 15:18:05 +03:00
Yury Shkoda bd1b58086d docs: left a comment regarding payload parts 2023-05-02 12:10:17 +03:00
Yury Shkoda 9f521634d9 chore(webout): added comment 2023-05-02 11:30:55 +03:00
Yury Shkoda a696168443 Merge branch 'main' of github.com:sasjs/server into issue-354 2023-05-02 11:17:41 +03:00
Yury Shkoda 31df72ad88 fix(webout): fixed adding empty webout to response payload 2023-05-02 11:17:12 +03:00
semantic-release-bot d2239f75c2 chore(release): 0.34.2 [skip ci]
## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01)

### Bug Fixes

* use custom logic for handling sequence ids ([dba53de](https://github.com/sasjs/server/commit/dba53de64664c9d8a40fe69de6281c53d1c73641))
2023-05-01 15:32:32 +00:00
Allan Bowe 45428892cc Merge pull request #362 from sasjs/remove-mongoose-sequence
fix: use custom logic for handling sequence ids
2023-05-01 16:28:47 +01:00
sabhas ac27a9b894 chore: remove residue 2023-05-01 19:54:43 +05:00
sabhas dba53de646 fix: use custom logic for handling sequence ids 2023-05-01 19:28:51 +05:00
Yury Shkoda eb42683fff feat(editor): parse print output in response payload 2023-05-01 08:18:49 +03:00
Yury Shkoda d2de9dc13e fix(editor): fixed log/webout/print tabs 2023-05-01 07:28:23 +03:00
Yury Shkoda 6dd2f4f876 fix(execution): removed empty webout from response 2023-04-28 17:25:30 +03:00
Yury Shkoda c0f38ba7c9 wip(print-output): added print output to response payload 2023-04-28 15:09:44 +03:00
semantic-release-bot d2f011e8a9 chore(release): 0.34.1 [skip ci]
## [0.34.1](https://github.com/sasjs/server/compare/v0.34.0...v0.34.1) (2023-04-28)

### Bug Fixes

* **css:** fixed css loading ([9c5acd6](https://github.com/sasjs/server/commit/9c5acd6de32afdbc186f79ae5b35375dda2e49b0))
* **log:** fixed chunk collapsing ([64b156f](https://github.com/sasjs/server/commit/64b156f7627969b7f13022726f984fbbfe1a33ef))
2023-04-28 11:50:19 +00:00
Yury Shkoda 5215633e96 Merge pull request #358 from sasjs/css-issue-fix
Css issue fix
2023-04-28 14:46:12 +03:00
Yury Shkoda 64b156f762 fix(log): fixed chunk collapsing 2023-04-28 13:30:25 +03:00
Yury Shkoda 9c5acd6de3 fix(css): fixed css loading 2023-04-28 13:29:31 +03:00
semantic-release-bot 3e72384a63 chore(release): 0.34.0 [skip ci]
# [0.34.0](https://github.com/sasjs/server/compare/v0.33.3...v0.34.0) (2023-04-28)

### Bug Fixes

* **log:** fixed checks for errors and warnings ([02e2b06](https://github.com/sasjs/server/commit/02e2b060f9bedf4806f45f5205fd87bfa2ecae90))
* **log:** fixed default runtime ([e04300a](https://github.com/sasjs/server/commit/e04300ad2ac237be7b28a6332fa87a3bcf761c7b))
* **log:** fixed parsing log for different runtime ([3b1e4a1](https://github.com/sasjs/server/commit/3b1e4a128b1f22ff6f3069f5aaada6bfb1b40d12))
* **log:** fixed scrolling issue ([56a522c](https://github.com/sasjs/server/commit/56a522c07c6f6d4c26c6d3b7cd6e9ef7007067a9))
* **log:** fixed single chunk display ([8254b78](https://github.com/sasjs/server/commit/8254b789555cb8bbb169f52b754b4ce24e876dd2))
* **log:** fixed single chunk scrolling ([57b7f95](https://github.com/sasjs/server/commit/57b7f954a17936f39aa9b757998b5b25e9442601))
* **log:** fixed switching runtime ([c7a7399](https://github.com/sasjs/server/commit/c7a73991a7aa25d0c75d0c00e712bdc78769300b))
* **log:** fixing switching from SAS to other runtime ([c72ecc7](https://github.com/sasjs/server/commit/c72ecc7e5943af9536ee31cfa85398e016d5354f))

### Features

* **log:** added download chunk and entire log ([a38a9f9](https://github.com/sasjs/server/commit/a38a9f9c3dfe36bd55d32024c166147318216995))
* **log:** added logComponent and LogTabWithIcons ([3a887de](https://github.com/sasjs/server/commit/3a887dec55371b6a00b92291bb681e4cccb770c0))
* **log:** added parseErrorsAndWarnings utility ([7c1c1e2](https://github.com/sasjs/server/commit/7c1c1e241002313c10f94dd61702584b9f148010))
* **log:** added time to downloaded log name ([3848bb0](https://github.com/sasjs/server/commit/3848bb0added69ca81a5c9419ea414bdd1c294bb))
* **log:** put download log icon into log tab ([777b3a5](https://github.com/sasjs/server/commit/777b3a55be1ecf5b05bf755ce8b14735496509e1))
* **log:** split large log into chunks ([75f5a3c](https://github.com/sasjs/server/commit/75f5a3c0b39665bef8b83dc7e1e8b3e5f23fc303))
* **log:** use improved log for SAS run time only ([7b12591](https://github.com/sasjs/server/commit/7b12591595cdd5144d9311ffa06a80c5dab79364))
2023-04-28 09:33:41 +00:00
Allan Bowe df5d40b445 Merge pull request #351 from sasjs/issue-346
Improve SAS log
2023-04-28 10:29:13 +01:00
semantic-release-bot c44ec35b3d chore(release): 0.33.3 [skip ci]
## [0.33.3](https://github.com/sasjs/server/compare/v0.33.2...v0.33.3) (2023-04-27)

### Bug Fixes

* use RateLimiterMemory instead of RateLimiterMongo ([6a520f5](https://github.com/sasjs/server/commit/6a520f5b26a3e2ed6345721b30ff4e3d9bfa903d))
2023-04-27 18:01:26 +00:00
Allan Bowe 77fac663c5 Merge pull request #357 from sasjs/cosmosdb-issue
fix: use RateLimiterMemory instead of RateLimiterMongo
2023-04-27 18:56:53 +01:00
Yury Shkoda 3848bb0add feat(log): added time to downloaded log name 2023-04-27 18:53:45 +03:00
Yury Shkoda 56a522c07c fix(log): fixed scrolling issue 2023-04-27 17:53:45 +03:00
Yury Shkoda 87e9172cfc chore(log): used css module to declare classes 2023-04-27 17:52:57 +03:00
sabhas 7df9588e66 chore: fixed specs 2023-04-27 16:26:43 +05:00
sabhas 6a520f5b26 fix: use RateLimiterMemory instead of RateLimiterMongo 2023-04-27 15:06:24 +05:00
Yury Shkoda 777b3a55be feat(log): put download log icon into log tab 2023-04-26 16:10:04 +03:00
semantic-release-bot 70c3834022 chore(release): 0.33.2 [skip ci]
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)

### Bug Fixes

* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
2023-04-24 21:13:55 +00:00
Allan Bowe dbf6c7de08 Merge pull request #355 from sasjs/issue274
fix: removing print redirection pending full #274 fix
2023-04-24 21:59:41 +01:00
allan d49ea47bd7 fix: removing print redirection pending full #274 fix 2023-04-24 21:58:13 +01:00
Yury Shkoda a38a9f9c3d feat(log): added download chunk and entire log 2023-04-21 17:21:09 +03:00
semantic-release-bot be4951d112 chore(release): 0.33.1 [skip ci]
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)

### Bug Fixes

* applying nologo only for sas.exe ([b4436ba](https://github.com/sasjs/server/commit/b4436bad0d24d5b5a402272632db1739b1018c90)), closes [#352](https://github.com/sasjs/server/issues/352)
2023-04-20 08:26:33 +00:00
Allan Bowe c116b263d9 Merge pull request #353 from sasjs/issue352
fix: applying nologo only for sas.exe
2023-04-20 09:22:29 +01:00
allan b4436bad0d fix: applying nologo only for sas.exe
Closes #352
2023-04-20 09:16:22 +01:00
Yury Shkoda 57b7f954a1 fix(log): fixed single chunk scrolling 2023-04-18 16:16:58 +03:00
Yury Shkoda 8254b78955 fix(log): fixed single chunk display 2023-04-18 15:46:53 +03:00
Yury Shkoda 75f5a3c0b3 feat(log): split large log into chunks 2023-04-18 11:42:10 +03:00
Yury Shkoda c72ecc7e59 fix(log): fixing switching from SAS to other runtime 2023-04-11 16:52:36 +03:00
Yury Shkoda e04300ad2a fix(log): fixed default runtime 2023-04-11 16:42:24 +03:00
Yury Shkoda c7a73991a7 fix(log): fixed switching runtime 2023-04-11 16:10:52 +03:00
Yury Shkoda 02e2b060f9 fix(log): fixed checks for errors and warnings 2023-04-11 15:21:46 +03:00
Yury Shkoda 3b1e4a128b fix(log): fixed parsing log for different runtime 2023-04-11 14:45:38 +03:00
Yury Shkoda 7b12591595 feat(log): use improved log for SAS run time only 2023-04-11 14:18:42 +03:00
Yury Shkoda 3a887dec55 feat(log): added logComponent and LogTabWithIcons 2023-04-10 16:21:32 +03:00
Yury Shkoda 7c1c1e2410 feat(log): added parseErrorsAndWarnings utility 2023-04-10 15:45:54 +03:00
Yury Shkoda 15774eca34 chore(deps): added react-highlight 2023-04-10 15:40:27 +03:00
semantic-release-bot 5e325522f4 chore(release): 0.33.0 [skip ci]
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)

### Features

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

### Features

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

### Features

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

* lint + remove default settings ([3de59ac](https://github.com/sasjs/server/commit/3de59ac4f8e3d95cad31f09e6963bd04c4811f26))

### Features

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

### Features

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

* update the response header of request to stp/execute routes ([112431a](https://github.com/sasjs/server/commit/112431a1b7461989c04100418d67d975a2a8f354))

### Features

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

### Features

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))

### Features

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

### Bug Fixes

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

### Features

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

### Features

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Features

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

### Bug Fixes

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

### Bug Fixes

* do not throw error on deleting group when it is created by an external auth provider ([68f0c5c](https://github.com/sasjs/server/commit/68f0c5c5884431e7e8f586dccf98132abebb193e))
* no need to restrict api endpoints when ldap auth is applied ([a142660](https://github.com/sasjs/server/commit/a14266077d3541c7a33b7635efa4208335e73519))
* remove authProvider attribute from user and group payload interface ([bbd7786](https://github.com/sasjs/server/commit/bbd7786c6ce13b374d896a45c23255b8fa3e8bd2))

### Features

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

### Features

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

### Features

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

### Features

* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
2022-09-05 12:21:34 +00:00
Allan Bowe cb82fea0d8 Merge pull request #264 from sasjs/mocker
Mocker
2022-09-05 13:16:10 +01:00
medjedovic b9a596616d chore: cleanup 2022-09-05 12:20:56 +02:00
Saad Jutt 7477326b22 chore: lower cased env values 2022-09-01 23:38:04 +05:00
Saad Jutt 76bf84316e chore: MOCK_SERVERTYPE instead of string literals 2022-09-01 23:34:57 +05:00
medjedovic 7633608318 chore: mocker architecture fix, env validation 2022-08-31 13:31:28 +02:00
medjedovic 572fe22d50 chore: mocksas9 controller 2022-08-30 17:27:37 +02:00
medjedovic 091268bf58 chore: mocking only mandatory bits from sas9 responses 2022-08-29 12:40:29 +02:00
medjedovic 71a4a48443 chore: generic sas9 mock responses 2022-08-29 10:30:01 +02:00
medjedovic 3b188cd724 style: lint 2022-08-26 18:03:28 +02:00
medjedovic eeba2328c0 chore: added login, logout endpoints 2022-08-26 17:59:07 +02:00
medjedovic 0a0ba2cca5 feat: added mocking endpoints 2022-08-25 15:58:08 +02:00
169 changed files with 23943 additions and 26789 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+3 -3
View File
@@ -5,7 +5,7 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
@@ -28,7 +28,7 @@ jobs:
run: npm run lint-web
build-api:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
@@ -66,7 +66,7 @@ jobs:
CI: true
build-web:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
+2 -2
View File
@@ -7,7 +7,7 @@ on:
jobs:
release:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
@@ -56,4 +56,4 @@ jobs:
- name: Release
run: |
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release
+1
View File
@@ -0,0 +1 @@
ignore-scripts=true
+1 -3
View File
@@ -1,5 +1,3 @@
{
"cSpell.words": [
"autoexec"
]
"cSpell.words": ["autoexec", "initialising"]
}
+505
View File
@@ -1,3 +1,508 @@
## [0.39.7](https://github.com/sasjs/server/compare/v0.39.6...v0.39.7) (2026-07-14)
### Bug Fixes
* **jsonwebtoken:** bumped version to avoid vulnerability ([1473925](https://github.com/sasjs/server/commit/1473925896db0e4472c9ef5ba64955527a1be2de))
## [0.39.6](https://github.com/sasjs/server/compare/v0.39.5...v0.39.6) (2026-07-14)
### Bug Fixes
* **api:** isolate drive.spec.ts's files folder from the real drive ([dec5191](https://github.com/sasjs/server/commit/dec51914911c10b66b43ebe95f77b0af63e2d03e))
* **api:** stop overwriting a failed JS/PY/R session back to completed ([4858245](https://github.com/sasjs/server/commit/4858245372fa19929b6e59c28148f5d004239c9d))
## [0.39.5](https://github.com/sasjs/server/compare/v0.39.4...v0.39.5) (2026-07-14)
### Bug Fixes
* **api:** harden mock SAS executable and timeouts for CI ([67fce47](https://github.com/sasjs/server/commit/67fce475a3c53b3c20919902edb45dce809dffb9))
* **api:** return prompt error response instead of hanging when a SAS session fails ([f84af4a](https://github.com/sasjs/server/commit/f84af4ac0639625fee4508e764f0eed07d7b4e8a))
* fix code.spec.ts failing on a fresh checkout (CI) ([0af6b63](https://github.com/sasjs/server/commit/0af6b63ffa770bfa2b77505d74778be517f8d631))
## [0.39.4](https://github.com/sasjs/server/compare/v0.39.3...v0.39.4) (2025-12-21)
### Bug Fixes
* **deps:** bump dependencies and resolve audit issues ([4f78202](https://github.com/sasjs/server/commit/4f782025dbcdfcbae6ca1fabb42ce1bc385e0162))
## [0.39.3](https://github.com/sasjs/server/compare/v0.39.2...v0.39.3) (2025-11-25)
### Bug Fixes
* (deps) bump @sasjs/core to 4.59.7 ([ab96653](https://github.com/sasjs/server/commit/ab966535642d08d4e8e984007b98c8fdffbe30f7))
* (deps) rerun npm i to sync ([225f381](https://github.com/sasjs/server/commit/225f381bdf8ad5aa2af8d75648df1dd5175e12e0))
## [0.39.2](https://github.com/sasjs/server/compare/v0.39.1...v0.39.2) (2025-09-25)
### Bug Fixes
* addressing test fail ([e51b204](https://github.com/sasjs/server/commit/e51b20421adc1598ea267c79b1fb4dbc085f97b9))
* packages missmatch ([379ea60](https://github.com/sasjs/server/commit/379ea604bcb5686b5299fae6a32f759c45b275ea))
* type libs ([6d123c3](https://github.com/sasjs/server/commit/6d123c3e23628c1d703eaa13142c77f0da970a55))
* typescript errors ([631e956](https://github.com/sasjs/server/commit/631e95604b64b1a96f2abade659348618f3b00b2))
* typescript errors ([198cd79](https://github.com/sasjs/server/commit/198cd79354254511c21ac1acfbf7b6bcfdab2af7))
## [0.39.1](https://github.com/sasjs/server/compare/v0.39.0...v0.39.1) (2025-03-13)
### Bug Fixes
* extra bit of sleep for file recognition ([f4768bf](https://github.com/sasjs/server/commit/f4768bffd3dbb2fe243966572ba74002024d96e1)), closes [#381](https://github.com/sasjs/server/issues/381)
# [0.39.0](https://github.com/sasjs/server/compare/v0.38.0...v0.39.0) (2024-10-31)
### Bug Fixes
* **api:** fixed condition in processProgram ([48a9a4d](https://github.com/sasjs/server/commit/48a9a4dd0e31f84209635382be4ec4bb2c3a9c0c))
### Features
* **api:** added session state endpoint ([6b6546c](https://github.com/sasjs/server/commit/6b6546c7ad0833347f8dc4cdba6ad19132f7aaef))
# [0.38.0](https://github.com/sasjs/server/compare/v0.37.0...v0.38.0) (2024-10-30)
### Features
* **api:** enabled query params in stp/trigger endpoint ([5cda9cd](https://github.com/sasjs/server/commit/5cda9cd5d8623b7ea2ecd989d7808f47ec866672))
# [0.37.0](https://github.com/sasjs/server/compare/v0.36.0...v0.37.0) (2024-10-29)
### Features
* **stp:** added trigger endpoint ([b0723f1](https://github.com/sasjs/server/commit/b0723f14448d60ffce4f2175cf8a73fc4d4dd0ee))
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)
### Features
* **code:** added code/trigger API endpoint ([ffcf193](https://github.com/sasjs/server/commit/ffcf193b87d811b166d79af74013776a253b50b0))
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)
### Bug Fixes
* **api:** fixed env issue in MacOS executable ([73d965d](https://github.com/sasjs/server/commit/73d965daf54b16c0921e4b18d11a1e6f8650884d))
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)
### Bug Fixes
* enable embedded LFs in JS STP vars ([7e8cbbf](https://github.com/sasjs/server/commit/7e8cbbf377b27a7f5dd9af0bc6605c01f302f5d9))
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)
### Bug Fixes
* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](https://github.com/sasjs/server/commit/9586dbb2d0d6611061c9efdfb84030144f62c2ee))
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)
### Bug Fixes
* **log-separator:** log separator should always wrap log ([8940f4d](https://github.com/sasjs/server/commit/8940f4dc47abae2036b4fcdeb772c31a0ca07cca))
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)
### Bug Fixes
* **editor:** fixed log/webout/print tabs ([d2de9dc](https://github.com/sasjs/server/commit/d2de9dc13ef2e980286dd03cca5e22cea443ed0c))
* **execute:** added atribute indicating stp api ([e78f87f](https://github.com/sasjs/server/commit/e78f87f5c00038ea11261dffb525ac8f1024e40b))
* **execute:** fixed adding print output ([9aaffce](https://github.com/sasjs/server/commit/9aaffce82051d81bf39adb69942bb321e9795141))
* **execution:** removed empty webout from response ([6dd2f4f](https://github.com/sasjs/server/commit/6dd2f4f87673336135bc7a6de0d2e143e192c025))
* **webout:** fixed adding empty webout to response payload ([31df72a](https://github.com/sasjs/server/commit/31df72ad88fe2c771d0ef8445d6db9dd147c40c9))
### Features
* **editor:** parse print output in response payload ([eb42683](https://github.com/sasjs/server/commit/eb42683fff701bd5b4d2b68760fe0c3ecad573dd))
## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01)
### Bug Fixes
* use custom logic for handling sequence ids ([dba53de](https://github.com/sasjs/server/commit/dba53de64664c9d8a40fe69de6281c53d1c73641))
## [0.34.1](https://github.com/sasjs/server/compare/v0.34.0...v0.34.1) (2023-04-28)
### Bug Fixes
* **css:** fixed css loading ([9c5acd6](https://github.com/sasjs/server/commit/9c5acd6de32afdbc186f79ae5b35375dda2e49b0))
* **log:** fixed chunk collapsing ([64b156f](https://github.com/sasjs/server/commit/64b156f7627969b7f13022726f984fbbfe1a33ef))
# [0.34.0](https://github.com/sasjs/server/compare/v0.33.3...v0.34.0) (2023-04-28)
### Bug Fixes
* **log:** fixed checks for errors and warnings ([02e2b06](https://github.com/sasjs/server/commit/02e2b060f9bedf4806f45f5205fd87bfa2ecae90))
* **log:** fixed default runtime ([e04300a](https://github.com/sasjs/server/commit/e04300ad2ac237be7b28a6332fa87a3bcf761c7b))
* **log:** fixed parsing log for different runtime ([3b1e4a1](https://github.com/sasjs/server/commit/3b1e4a128b1f22ff6f3069f5aaada6bfb1b40d12))
* **log:** fixed scrolling issue ([56a522c](https://github.com/sasjs/server/commit/56a522c07c6f6d4c26c6d3b7cd6e9ef7007067a9))
* **log:** fixed single chunk display ([8254b78](https://github.com/sasjs/server/commit/8254b789555cb8bbb169f52b754b4ce24e876dd2))
* **log:** fixed single chunk scrolling ([57b7f95](https://github.com/sasjs/server/commit/57b7f954a17936f39aa9b757998b5b25e9442601))
* **log:** fixed switching runtime ([c7a7399](https://github.com/sasjs/server/commit/c7a73991a7aa25d0c75d0c00e712bdc78769300b))
* **log:** fixing switching from SAS to other runtime ([c72ecc7](https://github.com/sasjs/server/commit/c72ecc7e5943af9536ee31cfa85398e016d5354f))
### Features
* **log:** added download chunk and entire log ([a38a9f9](https://github.com/sasjs/server/commit/a38a9f9c3dfe36bd55d32024c166147318216995))
* **log:** added logComponent and LogTabWithIcons ([3a887de](https://github.com/sasjs/server/commit/3a887dec55371b6a00b92291bb681e4cccb770c0))
* **log:** added parseErrorsAndWarnings utility ([7c1c1e2](https://github.com/sasjs/server/commit/7c1c1e241002313c10f94dd61702584b9f148010))
* **log:** added time to downloaded log name ([3848bb0](https://github.com/sasjs/server/commit/3848bb0added69ca81a5c9419ea414bdd1c294bb))
* **log:** put download log icon into log tab ([777b3a5](https://github.com/sasjs/server/commit/777b3a55be1ecf5b05bf755ce8b14735496509e1))
* **log:** split large log into chunks ([75f5a3c](https://github.com/sasjs/server/commit/75f5a3c0b39665bef8b83dc7e1e8b3e5f23fc303))
* **log:** use improved log for SAS run time only ([7b12591](https://github.com/sasjs/server/commit/7b12591595cdd5144d9311ffa06a80c5dab79364))
## [0.33.3](https://github.com/sasjs/server/compare/v0.33.2...v0.33.3) (2023-04-27)
### Bug Fixes
* use RateLimiterMemory instead of RateLimiterMongo ([6a520f5](https://github.com/sasjs/server/commit/6a520f5b26a3e2ed6345721b30ff4e3d9bfa903d))
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)
### Bug Fixes
* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
### Bug Fixes
* applying nologo only for sas.exe ([b4436ba](https://github.com/sasjs/server/commit/b4436bad0d24d5b5a402272632db1739b1018c90)), closes [#352](https://github.com/sasjs/server/issues/352)
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)
### Features
* option to reset admin password on startup ([eda8e56](https://github.com/sasjs/server/commit/eda8e56bb0ea20fdaacabbbe7dcf1e3ea7bd215a))
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)
### Features
* add an api endpoint for admin to get list of client ids ([6ffaa7e](https://github.com/sasjs/server/commit/6ffaa7e9e2a62c083bb9fcc3398dcbed10cebdb1))
# [0.31.0](https://github.com/sasjs/server/compare/v0.30.3...v0.31.0) (2023-03-30)
### Features
* prevent brute force attack by rate limiting login endpoint ([a82cabb](https://github.com/sasjs/server/commit/a82cabb00134c79c5ee77afd1b1628a1f768e050))
## [0.30.3](https://github.com/sasjs/server/compare/v0.30.2...v0.30.3) (2023-03-07)
### Bug Fixes
* add location.pathname to location.origin conditionally ([edab51c](https://github.com/sasjs/server/commit/edab51c51997f17553e037dc7c2b5e5fa6ea8ffe))
## [0.30.2](https://github.com/sasjs/server/compare/v0.30.1...v0.30.2) (2023-03-07)
### Bug Fixes
* **web:** add path to base in launch program url ([2c31922](https://github.com/sasjs/server/commit/2c31922f58a8aa20d7fa6bfc95b53a350f90c798))
## [0.30.1](https://github.com/sasjs/server/compare/v0.30.0...v0.30.1) (2023-03-01)
### Bug Fixes
* **web:** add proper base url in axios.defaults ([5e3ce8a](https://github.com/sasjs/server/commit/5e3ce8a98f1825e14c1d26d8da0c9821beeff7b3))
# [0.30.0](https://github.com/sasjs/server/compare/v0.29.0...v0.30.0) (2023-02-28)
### Bug Fixes
* lint + remove default settings ([3de59ac](https://github.com/sasjs/server/commit/3de59ac4f8e3d95cad31f09e6963bd04c4811f26))
### Features
* add new env config DB_TYPE ([158f044](https://github.com/sasjs/server/commit/158f044363abf2576c8248f0ca9da4bc9cb7e9d8))
# [0.29.0](https://github.com/sasjs/server/compare/v0.28.7...v0.29.0) (2023-02-06)
### Features
* Add /SASjsApi endpoint in permissions ([b3402ea](https://github.com/sasjs/server/commit/b3402ea80afb8802eee8b8b6cbbbcc29903424bc))
## [0.28.7](https://github.com/sasjs/server/compare/v0.28.6...v0.28.7) (2023-02-03)
### Bug Fixes
* add user to all users group on user creation ([2bae52e](https://github.com/sasjs/server/commit/2bae52e307327d7ee4a94b19d843abdc0ccec9d1))
## [0.28.6](https://github.com/sasjs/server/compare/v0.28.5...v0.28.6) (2023-01-26)
### Bug Fixes
* show loading spinner on login screen while request is in process ([69f2576](https://github.com/sasjs/server/commit/69f2576ee6d3d7b7f3325922a88656d511e3ac88))
## [0.28.5](https://github.com/sasjs/server/compare/v0.28.4...v0.28.5) (2023-01-01)
### Bug Fixes
* adding NOPRNGETLIST system option for faster startup ([96eca3a](https://github.com/sasjs/server/commit/96eca3a35dce4521150257ee019beb4488c8a08f))
## [0.28.4](https://github.com/sasjs/server/compare/v0.28.3...v0.28.4) (2022-12-07)
### Bug Fixes
* replace main class with container class ([71c429b](https://github.com/sasjs/server/commit/71c429b093b91e2444ae75d946579dccc2e48636))
## [0.28.3](https://github.com/sasjs/server/compare/v0.28.2...v0.28.3) (2022-12-06)
### Bug Fixes
* stringify json file ([1192583](https://github.com/sasjs/server/commit/1192583843d7efd1a6ab6943207f394c3ae966be))
## [0.28.2](https://github.com/sasjs/server/compare/v0.28.1...v0.28.2) (2022-12-05)
### Bug Fixes
* execute child process asyncronously ([23c997b](https://github.com/sasjs/server/commit/23c997b3beabeb6b733ae893031d2f1a48f28ad2))
* JS / Python / R session folders should be NEW folders, not existing SAS folders ([39ba995](https://github.com/sasjs/server/commit/39ba995355daa24bb7ab22720f8fc57d2dc85f40))
## [0.28.1](https://github.com/sasjs/server/compare/v0.28.0...v0.28.1) (2022-11-28)
### Bug Fixes
* update the content type header after the program has been executed ([4dcee4b](https://github.com/sasjs/server/commit/4dcee4b3c3950d402220b8f451c50ad98a317d83))
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)
### Bug Fixes
* update the response header of request to stp/execute routes ([112431a](https://github.com/sasjs/server/commit/112431a1b7461989c04100418d67d975a2a8f354))
### Features
* **api:** add the api endpoint for updating user password ([4581f32](https://github.com/sasjs/server/commit/4581f325344eb68c5df5a28492f132312f15bb5c))
* ask for updated password on first login ([1d48f88](https://github.com/sasjs/server/commit/1d48f8856b1fbbf3ef868914558333190e04981f))
* **web:** add the UI for updating user password ([8b8c43c](https://github.com/sasjs/server/commit/8b8c43c21bde5379825c5ec44ecd81a92425f605))
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)
### Features
* on startup add webout.sas file in sasautos folder ([200f6c5](https://github.com/sasjs/server/commit/200f6c596a6e732d799ed408f1f0fd92f216ba58))
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)
### Bug Fixes
* comments ([7ae862c](https://github.com/sasjs/server/commit/7ae862c5ce720e9483d4728f4295dede4f849436))
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)
### Bug Fixes
* change the expiration of access/refresh tokens from days to seconds ([bb05493](https://github.com/sasjs/server/commit/bb054938c5bd0535ae6b9da93ba0b14f9b80ddcd))
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)
### Bug Fixes
* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))
### Features
* make access token duration configurable when creating client/secret ([2413c05](https://github.com/sasjs/server/commit/2413c05fea3960f7e5c3c8b7b2f85d61314f08db))
* make refresh token duration configurable ([abd5c64](https://github.com/sasjs/server/commit/abd5c64b4a726e3f17594a98111b6aa269b71fee))
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
### Bug Fixes
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
### Features
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
### Features
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
### Bug Fixes
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
### Bug Fixes
* added domain for session cookies ([94072c3](https://github.com/sasjs/server/commit/94072c3d24a4d0d4c97900dc31bfbf1c9d2559b7))
## [0.23.2](https://github.com/sasjs/server/compare/v0.23.1...v0.23.2) (2022-10-06)
### Bug Fixes
* bump in correct place ([14731e8](https://github.com/sasjs/server/commit/14731e8824fa9f3d1daf89fd62f9916d5e3fcae4))
* bumping sasjs/score ([258cc35](https://github.com/sasjs/server/commit/258cc35f14cf50f2160f607000c60de27593fd79))
* reverting commit ([fda0e0b](https://github.com/sasjs/server/commit/fda0e0b57d56e3b5231e626a8d933343ac0c5cdc))
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)
### Bug Fixes
* ldap issues ([4d64420](https://github.com/sasjs/server/commit/4d64420c45424134b4d2014a2d5dd6e846ed03b3))
# [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)
### Features
* Enable SAS_PACKAGES in SASjs Server ([424f0fc](https://github.com/sasjs/server/commit/424f0fc1faec765eb7a14619584e649454105b70))
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)
### Bug Fixes
* spelling issues ([3bb0597](https://github.com/sasjs/server/commit/3bb05974d216d69368f4498eb9f309bce7d97fd8))
# [0.22.0](https://github.com/sasjs/server/compare/v0.21.7...v0.22.0) (2022-10-03)
### Bug Fixes
* do not throw error on deleting group when it is created by an external auth provider ([68f0c5c](https://github.com/sasjs/server/commit/68f0c5c5884431e7e8f586dccf98132abebb193e))
* no need to restrict api endpoints when ldap auth is applied ([a142660](https://github.com/sasjs/server/commit/a14266077d3541c7a33b7635efa4208335e73519))
* remove authProvider attribute from user and group payload interface ([bbd7786](https://github.com/sasjs/server/commit/bbd7786c6ce13b374d896a45c23255b8fa3e8bd2))
### Features
* implemented LDAP authentication ([f915c51](https://github.com/sasjs/server/commit/f915c51b077a2b8c4099727355ed914ecd6364bd))
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)
### Bug Fixes
* csrf package is changed to pillarjs-csrf ([fe3e508](https://github.com/sasjs/server/commit/fe3e5088f8dfff50042ec8e8aac9ba5ba1394deb))
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)
### Bug Fixes
* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](https://github.com/sasjs/server/commit/40f95f9072c8685910138d88fd2410f8704fc975))
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)
### Bug Fixes
* made files extensions case insensitive ([2496043](https://github.com/sasjs/server/commit/249604384e42be4c12c88c70a7dff90fc1917a8f))
## [0.21.4](https://github.com/sasjs/server/compare/v0.21.3...v0.21.4) (2022-09-21)
### Bug Fixes
* removing single quotes from _program value ([a0e7875](https://github.com/sasjs/server/commit/a0e7875ae61cbb6e7d3995d2e36e7300b0daec86))
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)
### Bug Fixes
* return same tokens if not expired ([330c020](https://github.com/sasjs/server/commit/330c020933f1080261b38f07d6b627f6d7c62446))
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)
### Bug Fixes
* default content-type for sas programs should be text/plain ([9977c9d](https://github.com/sasjs/server/commit/9977c9d161947b11d45ab2513f99a5320a3f5a06))
* **studio:** inject program path to code before sending for execution ([edc2e2a](https://github.com/sasjs/server/commit/edc2e2a302ccea4985f3d6b83ef8c23620ab82b6))
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)
### Bug Fixes
* SASJS_WEBOUT_HEADERS path for windows ([0749d65](https://github.com/sasjs/server/commit/0749d65173e8cfe9a93464711b7be1e123c289ff))
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)
### Features
* sas9 mocker improved - public access denied scenario ([06d3b17](https://github.com/sasjs/server/commit/06d3b1715432ea245ee755ae1dfd0579d3eb30e9))
# [0.20.0](https://github.com/sasjs/server/compare/v0.19.0...v0.20.0) (2022-09-16)
### Features
* add support for R stored programs ([d6651bb](https://github.com/sasjs/server/commit/d6651bbdbeee5067f53c36e69a0eefa973c523b6))
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)
### Features
* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)
+60 -6
View File
@@ -66,14 +66,14 @@ MODE=
# A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas and js
# Possible options at the moment are sas, js, py and r
# This string sets the priority of the available analytic runtimes
# Valid runtimes are SAS (sas), JavaScript (js) and Python (py)
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
# For each option provided, there should be a corresponding path,
# eg SAS_PATH, NODE_PATH or PYTHON_PATH
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
# Priority is given to runtimes earlier in the string
# Example options: [sas,js,py | js,py | sas | sas,js]
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
RUN_TIMES=
# Path to SAS executable (sas.exe / sas.sh)
@@ -85,17 +85,32 @@ NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
# Path to Python executable
PYTHON_PATH=/usr/bin/python
# Path to R executable
R_PATH=/usr/bin/Rscript
# Path to working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root
# This location is for files, sasjs packages and appStreamConfig.json
DRIVE_LOCATION=./sasjs_root/drive
# options: [http|https] default: http
PROTOCOL=
# default: 5000
PORT=
# options: [sas9|sasviya]
# If not present, mocking function is disabled
MOCK_SERVERTYPE=
# default: /api/mocks
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
# Server will automatically use subdirectory accordingly
STATIC_MOCK_LOCATION=
#
## Additional SAS Options
@@ -119,9 +134,22 @@ PRIVATE_KEY=privkey.pem (required)
CERT_CHAIN=certificate.pem (required)
CA_ROOT=fullchain.pem (optional)
# ENV variables required for MODE: `server`
## ENV variables required for MODE: `server`
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
# options: [mongodb|cosmos_mongodb] default: mongodb
DB_TYPE=
# AUTH_PROVIDERS options: [ldap] default: ``
AUTH_PROVIDERS=
## ENV variables required for AUTH_MECHANISM: `ldap`
LDAP_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
LDAP_BIND_PASSWORD = <password>
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
@@ -130,7 +158,7 @@ CORS=
WHITELIST=
# HELMET Cross Origin Embedder Policy
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# options: [true|false] default: true
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
HELMET_COEP=
@@ -147,6 +175,32 @@ HELMET_COEP=
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
# To prevent brute force attack on login route we have implemented rate limiter
# Only valid for MODE: server
# Following are configurable env variable rate limiter
# After this, access is blocked for 1 day
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
# After this, access is blocked for an hour
# Store number for 24 days since first fail
# Once a successful login is attempted, it resets
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
# Name of the admin user that will be created on startup if not exists already
# Default is `secretuser`
ADMIN_USERNAME=secretuser
# Temporary password for the ADMIN_USERNAME, which is in place until the first login
# Default is `secretpassword`
ADMIN_PASSWORD_INITIAL=secretpassword
# Specify whether app has to reset the ADMIN_USERNAME's password or not
# Default is NO. Possible options are YES and NO
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
ADMIN_PASSWORD_RESET=NO
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=
+22
View File
@@ -1,5 +1,6 @@
MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
ALLOWED_DOMAIN=<just domain e.g. example.com >
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http
@@ -13,13 +14,34 @@ 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
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
DB_TYPE=[mongodb|cosmos_mongodb] default considered as mongodb
AUTH_PROVIDERS=[ldap]
LDAP_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
LDAP_BIND_PASSWORD = <password>
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
#default value is 100
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
#default value is 10
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
ADMIN_USERNAME=secretuser
ADMIN_PASSWORD_INITIAL=secretpassword
ADMIN_PASSWORD_RESET=NO
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
PYTHON_PATH=/usr/bin/python
R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root
DRIVE_LOCATION=./sasjs_root/drive
LOG_FORMAT_MORGAN=common
LOG_LOCATION=./sasjs_root/logs
@@ -0,0 +1,38 @@
# Code execution & session diagrams
Mermaid diagrams describing how `@sasjs/server` executes submitted code
(SAS/JS/PY/R) against pooled "sessions". Written for fast context-loading by
an AI coding agent: each diagram is self-contained, node/edge labels carry
the actual file:line references, and prose is kept to the minimum needed to
disambiguate the diagram.
| File | Covers |
|---|---|
| [session-lifecycle.md](session-lifecycle.md) | `SessionState` state machine; how it differs between the SAS runtime and the JS/PY/R runtimes |
| [sas-execution-handshake.md](sas-execution-handshake.md) | The SYSIN/AUTOEXEC file-swap mechanism a SAS session uses to turn one long-lived `sas` process into a single-use execution slot |
| [request-execution-flow.md](request-execution-flow.md) | End-to-end flowchart from HTTP request to response, covering session-pool acquisition and both runtime branches (SAS vs JS/PY/R) |
## Core concept
A "session" is not a request-scoped object. It is a pooled, reusable
execution slot with a filesystem folder (`session.path`) and a state
(`SessionState`). For the SAS runtime specifically, a session also owns one
real OS process, spawned at session-*creation* time and consumed by exactly
one code submission (see `sas-execution-handshake.md`). For JS/PY/R, a
session is just an ID + folder; the actual interpreter process is spawned
fresh per request inside `processProgram`.
## Key source files
- `api/src/controllers/internal/Session.ts` — session pool + lifecycle
(`SessionController`, `SASSessionController`), `SessionState` transitions.
- `api/src/controllers/internal/processProgram.ts` — writes the submitted
code into the session and drives it to completion/failure, per runtime.
- `api/src/controllers/internal/Execution.ts``ExecutionController`,
the entry point controllers call; acquires a session, calls
`processProgram`, reads back `log.log`/`webout.txt`/headers, builds the
HTTP response (or a `SessionExecutionError` on failure).
- `api/src/controllers/internal/create{SAS,JS,Python,R}Program.ts`
per-runtime code templating (wraps the user's submitted code with
boilerplate: variable injection, `_webout` redirection, etc).
- `api/src/types/Session.ts``SessionState` enum and `Session` interface.
@@ -0,0 +1,64 @@
# Request execution flow (end-to-end)
Full path from an inbound HTTP request to the HTTP response, covering
session-pool acquisition and the two structurally different runtime
branches: SAS (reuses a parked long-lived process) vs JS/PY/R (spawns a
fresh interpreter process per request).
```mermaid
flowchart TD
A["HTTP request<br/>POST /SASjsApi/code/execute<br/>or /SASjsApi/stp/execute"] --> B["Controller<br/>code.ts / stp.ts<br/>try/catch wrapper"]
B --> C["ExecutionController.executeProgram<br/>or .executeFile<br/>Execution.ts:58-91"]
C --> D{"session already provided?<br/>(e.g. file-upload flow)"}
D -- no --> E["getSessionController(runTime).getSession()<br/>Session.ts:59-69"]
D -- yes --> F["use provided session"]
E --> G{"pending session<br/>available in pool?"}
G -- yes --> H["reuse it"]
G -- no --> I["createSession()<br/>SAS: spawns a real sas process, see<br/>sas-execution-handshake.md<br/>JS/PY/R: cheap - folder + id only"]
H --> J["pre-warm: if pool has fewer<br/>than 3 pending, fire off more<br/>createSession() calls (not awaited)<br/>Session.ts:66"]
I --> J
F --> K
J --> K["session.state = running<br/>Execution.ts:96"]
K --> L["write webout.txt (empty),<br/>reqHeaders.txt<br/>Execution.ts:103-107"]
L --> M["processProgram(...)<br/>processProgram.ts:16-27"]
M --> N{"runTime?"}
N -- SAS --> O["createSASProgram():<br/>wrap user code with _webout<br/>filename, macro vars, autoexec"]
O --> P["write code.sas via .bkp + rename<br/>processProgram.ts:48-49"]
P --> Q["poll: while session.state !== completed<br/>processProgram.ts:52-58"]
Q -- "state becomes completed" --> R["resolve"]
Q -- "state becomes failed" --> S["throw Error(session.failureReason)"]
N -- "JS / PY / R" --> T["createJSProgram / createPythonProgram /<br/>createRProgram: wrap user code similarly"]
T --> U["write code file; spawn interpreter<br/>fresh via execFile; pipe stdout/stderr<br/>into a log.log write stream<br/>processProgram.ts:108-134"]
U -- "exit 0" --> R
U -- "exit non-zero" --> S
R --> V["read log.log, webout.txt,<br/>stpsrv_header.txt<br/>Execution.ts:128-131"]
V --> W["build httpHeaders + result<br/>return ExecuteReturnRaw<br/>Execution.ts:128-151"]
W --> X["Controller: res.send(result)<br/>HTTP 200"]
S --> Y["catch in Execution.ts:109-126:<br/>read whatever log exists,<br/>throw SessionExecutionError(message, log)"]
Y --> Z["Controller catch block<br/>rethrow { code: 400, status: 'failure',<br/>message, error, log }"]
Z --> AA["res.status(err.code).send(err)<br/>HTTP 400 with complete log"]
```
## Notes
- **Session pool is per-runtime.** SAS sessions and JS/PY/R sessions live in
separate controllers/pools (`process.sasSessionController` vs
`process.sessionController`, `Session.ts:239-253`) - a pending SAS session
is never handed out for a JS request or vice versa.
- **The SAS branch and the JS/PY/R branch converge** on the same
success/failure signal shape: `session.state` becomes `completed` or
`failed` either way, and both paths flow through the same log/webout
reading logic in `Execution.ts` afterward. The mechanics of *how* that
state gets set differ substantially (see `session-lifecycle.md`).
- **`includePrintOutput`** (SAS only) additionally appends `output.lst`
content to the result when debug mode is on - omitted above for brevity;
see `Execution.ts:135-142`.
- **`triggerProgram`/`triggerCode`** (fire-and-forget variants, not shown)
call the same `ExecutionController` methods without awaiting them and
immediately return `{ sessionId }`; the client polls
`GET /SASjsApi/session/{sessionId}/state` separately.
@@ -0,0 +1,81 @@
# SAS session execution handshake
How one long-lived `sas` process is turned into a single-use execution slot
via a SYSIN file-swap trick. This is the mechanism specific to
`RunTimeType.SAS`; JS/PY/R sessions spawn a fresh interpreter process per
request instead (see `request-execution-flow.md`).
```mermaid
sequenceDiagram
participant Client as HTTP client
participant Ctrl as Controller<br/>(code.ts / stp.ts)
participant Exec as ExecutionController<br/>(Execution.ts)
participant Pool as SASSessionController<br/>(Session.ts)
participant FS as Filesystem<br/>(session.path)
participant SAS as spawned "sas" process
Note over Pool,SAS: SESSION CREATION - happens ahead of any request,<br/>pool pre-warms up to 3 sessions (Session.ts:59-69)
Pool->>FS: createFile(code.sas, "") - empty dummy SYSIN (Session.ts:117-118)
Pool->>FS: readFile(sysInitCompiledPath) - compiled system-init<br/>macros, produced by `npm run compileSysInit` (Session.ts:105)
Note over Pool: throws if this file doesn't exist yet -<br/>session creation never reaches execFile below
Pool->>FS: createFile(autoexec.sas, autoExecContent) (Session.ts:108-114)
Pool->>SAS: execFile(sasLoc, -SYSIN code.sas -AUTOEXEC autoexec.sas -LOG log.log ...) (Session.ts:127-147)
activate SAS
Note right of SAS: process stays active (this box)<br/>from spawn until it finally exits below
SAS->>FS: autoexec step 1: delete code.sas, the dummy SYSIN (Session.ts:257-261)
Pool->>FS: waitForSession() polls fileExists(code.sas) (Session.ts:175-192)
FS-->>Pool: code.sas no longer exists
Pool->>Pool: session.state = pending (Session.ts:190)
SAS->>SAS: autoexec step 2: busy-wait for code.sas to reappear,<br/>up to 15 minutes (Session.ts:263-274)
Note over SAS: process is now idle, parked mid-autoexec,<br/>waiting on the filesystem.<br/>Counts as "pending" in the pool.
Note over Client,SAS: REQUEST ARRIVES - reuses the parked process above
Client->>Ctrl: POST .../execute { code, runTime: sas }
Ctrl->>Exec: executeProgram({ program, runTime: SAS, ... })
Exec->>Pool: getSession() returns the pending session (Session.ts:59-69)
Exec->>Exec: session.state = running (Execution.ts:96)
Exec->>FS: createFile(webout.txt, "") and createFile(reqHeaders.txt, ...) (Execution.ts:103-107)
Exec->>Pool: processProgram(program, session, ...) (Execution.ts:110-121)
Pool->>FS: createSASProgram() wraps user code with macro vars,<br/>_webout filename, autoexec injection (createSASProgram.ts)
Pool->>FS: write code.sas.bkp, then rename to code.sas (processProgram.ts:48-49)
Note over FS: write-then-rename, not a direct write,<br/>so SAS never reads a partial file
FS-->>SAS: code.sas exists again
SAS->>SAS: autoexec step 2 loop exits, sleeps 0.01s, autoexec ends (Session.ts:267-271)
Note over SAS: -SYSIN has pointed at code.sas from the start,<br/>so SAS now executes ITS CONTENT as the main job
SAS->>FS: writes log.log, output.lst, webout.txt while running
Pool->>Pool: processProgram poll loop:<br/>while session.state !== completed (processProgram.ts:52-58)
alt program reaches EOF normally
SAS->>SAS: exits with code 0
SAS-->>Pool: execFilePromise resolves - .then() (Session.ts:148-152)
Pool->>Pool: session.state = completed
Pool-->>Exec: processProgram() resolves
else program aborts (fatal error, license failure, hard STOP)
SAS->>SAS: exits with non-zero code
SAS-->>Pool: execFilePromise rejects - .catch() (Session.ts:153-163)
Pool->>Pool: session.state = failed<br/>session.failureReason = err.toString()
Pool-->>Exec: processProgram() throws (processProgram.ts:53-55)
end
deactivate SAS
Exec->>FS: read log.log, webout.txt, stpsrv_header.txt (Execution.ts:123, 128-131)
Exec-->>Ctrl: { httpHeaders, result }<br/>or throws SessionExecutionError{ message, log } (Execution.ts:109-126)
Ctrl-->>Client: 200 + result<br/>or 400 { status, message, error, log }
```
## Why this design
- SAS has meaningful startup cost (loading the engine, running system init
macros). Spawning a fresh `sas` process per request would pay that cost
every time. Instead, a process is spawned once and parked, ready to run
exactly one job the moment code shows up at a path it's already watching.
- The pool (`SessionController.getSession`, `Session.ts:59-69`) keeps up to
3 such parked processes ready, so most requests get an instant handoff
instead of waiting through the ~15-minute autoexec spin-wait or a cold
start.
- A session is single-use: once its process consumes the real SYSIN content
and exits (success or failure), that process is gone. The session object
itself is deleted later by `scheduleSessionDestroy` (`Session.ts:204-236`),
not reused for a second job.
+70
View File
@@ -0,0 +1,70 @@
# Session lifecycle (`SessionState`)
Enum defined in `api/src/types/Session.ts`. A `Session` is `{ id, state,
path, creationTimeStamp, deathTimeStamp, expiresAfterMins?, failureReason? }`.
Sessions live in an in-memory array on a per-runtime singleton controller
(`process.sasSessionController` for SAS, `process.sessionController` shared
by JS/PY/R — see `getSessionController()` in `Session.ts:239-253`).
```mermaid
stateDiagram-v2
[*] --> initialising: SAS runtime<br/>SASSessionController.createSession()<br/>Session.ts:77-95
[*] --> pending: JS/PY/R runtime, no process yet<br/>SessionController.createSession()<br/>Session.ts:30-57
initialising --> pending: dummy SYSIN file deleted<br/>by SAS's autoexec<br/>waitForSession(), Session.ts:175-192
initialising --> failed: spawned SAS process exits<br/>before handshake completes<br/>Session.ts:153-163, 184-188
pending --> running: session picked for a request<br/>executeProgram(), Execution.ts:94-96
running --> completed: process exits 0<br/>SAS: Session.ts:148-152 (original process)<br/>JS/PY/R: processProgram.ts:116-120 (fresh process)
running --> failed: process exits non-zero<br/>SAS: Session.ts:153-163<br/>JS/PY/R: processProgram.ts:121-131<br/>failureReason = err.toString()
completed --> [*]: deleteSession()<br/>scheduleSessionDestroy(), Session.ts:194-236
failed --> [*]: deleteSession()<br/>scheduleSessionDestroy()
note right of initialising
SAS only. The real sas
executable is already
running, spin-waiting
inside its AUTOEXEC for
real code to arrive.
See sas-execution-handshake.md
end note
note right of pending
Session sits in the pool,
reusable. Pre-warmed up to
3 pending sessions per
runtime (getSession(),
Session.ts:59-69)
end note
note right of running
SAS: no new process spawned
here, request just feeds
code to the already-running
process.
JS/PY/R: a brand new
interpreter process is
spawned right now.
end note
```
## Consumers of `state`
| Reader | Location | Watches for |
|---|---|---|
| `waitForSession` | `Session.ts:175-192` | `failed` (breaks early) or the dummy SYSIN file disappearing (implies session survived init) |
| `processProgram` (SAS branch poll loop) | `processProgram.ts:52-58` | `completed` (success exit) or `failed` (throws, carrying `session.failureReason`) |
| `scheduleSessionDestroy` | `Session.ts:204-236` | `running` (extends death timer instead of destroying) |
## Asymmetry between runtimes
- **SAS**: one OS process per session, spawned once at `createSession()`
time and reused for exactly one job (see `sas-execution-handshake.md`).
`running`/`completed`/`failed` are all driven by that *same* process's
eventual exit.
- **JS/PY/R**: a session is cheap (folder + id, no process). The interpreter
process is spawned fresh, per request, inside `processProgram.ts:115` and
its exit drives `completed`/`failed` directly in the same function - there
is no separate poll loop for these runtimes.
+1
View File
@@ -0,0 +1 @@
You have signed in.
+1
View File
@@ -0,0 +1 @@
You have signed out.
+30
View File
@@ -0,0 +1,30 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1" />
</head>
<div class="content">
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
<!--form container-->
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
<span class="userid">
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
</span>
<span class="password">
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
</span>
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
</form>
</div>
</html>
@@ -0,0 +1 @@
Public access has been denied.
@@ -0,0 +1 @@
"title": "Log Off SAS Demo User"
+6693 -12430
View File
File diff suppressed because it is too large Load Diff
+36 -34
View File
@@ -4,33 +4,32 @@
"description": "Api of SASjs server",
"main": "./src/server.ts",
"scripts": {
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore && npm run downloadMacros",
"prestart": "npm run initial",
"prebuild": "npm run initial",
"start": "NODE_ENV=development nodemon ./src/server.ts",
"start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc",
"postbuild": "npm run copy:files",
"build": "npm run initial && rimraf build && tsc && npm run copy:files",
"swagger": "tsoa spec",
"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}\"",
"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/",
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sas:copy && npm run web:copy",
"public:copy": "cpr ./public/ ./build/public/",
"sasjsbuild:copy": "cpr ./sasjsbuild/ ./build/sasjsbuild/",
"sas:copy": "cpr ./sas/ ./build/sas/",
"web:copy": "rimraf web && mkdir web && cpr ../web/build/ ./web/build/",
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts",
"downloadMacros": "ts-node ./scripts/downloadMacros.ts"
},
"bin": "./build/src/server.js",
"pkg": {
"assets": [
"./build/public/**/*",
"./build/sasjsbuild/**/*",
"./build/sasjscore/**/*",
"./build/sas/**/*",
"./web/build/**/*"
],
"targets": [
@@ -47,25 +46,25 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.31.3",
"@sasjs/utils": "2.42.1",
"@sasjs/core": "^4.59.10",
"@sasjs/utils": "^3.5.6",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"express": "^4.21.2",
"express-session": "^1.18.2",
"helmet": "^5.0.2",
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"jsonwebtoken": "^9.0.3",
"ldapjs": "2.3.3",
"mongoose": "^6.13.8",
"morgan": "^1.10.1",
"multer": "^1.4.5-lts.1",
"rate-limiter-flexible": "2.4.1",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"unzipper": "^0.12.3",
"url": "^0.10.3"
},
"devDependencies": {
@@ -73,32 +72,35 @@
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.5",
"@types/mongoose-sequence": "^3.0.6",
"@types/ldapjs": "^2.2.4",
"@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
"@types/node": "^20.0.0",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"dotenv": "^10.0.0",
"axios": "1.12.2",
"cpr": "^3.0.1",
"csrf": "^3.1.0",
"dotenv": "^16.0.1",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7",
"jest": "^29.7.0",
"mongodb-memory-server": "8.11.4",
"nodejs-file-downloader": "4.10.2",
"nodemon": "^3.0.0",
"pkg": "5.6.0",
"prettier": "^2.3.1",
"prettier": "^3.0.3",
"rimraf": "^3.0.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-jest": "^29.1.0",
"ts-node": "^10.0.0",
"tsoa": "3.14.1",
"typescript": "^4.3.2"
"typescript": "^5.0.0"
},
"nodemonConfig": {
"ignore": [
+2100 -1971
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
import axios from 'axios'
import Downloader from 'nodejs-file-downloader'
import { createFile, listFilesInFolder } from '@sasjs/utils'
import { sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils/file'
export const downloadMacros = async () => {
const url =
'https://api.github.com/repos/yabwon/SAS_PACKAGES/contents/SPF/Macros'
console.info(`Downloading macros from ${url}`)
await axios
.get(url)
.then(async (res) => {
await downloadFiles(res.data)
})
.catch((err) => {
throw new Error(err)
})
}
const downloadFiles = async function (fileList: any) {
for (const file of fileList) {
const downloader = new Downloader({
url: file.download_url,
directory: sasJSCoreMacros,
fileName: file.path.replace(/^SPF\/Macros/, ''),
cloneFiles: false
})
await downloader.download()
}
const fileNames = await listFilesInFolder(sasJSCoreMacros)
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
}
downloadMacros()
+1 -1
View File
@@ -15,7 +15,7 @@ export const configureCors = (app: Express) => {
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
process.logger.info('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
}
+24 -8
View File
@@ -1,22 +1,38 @@
import { Express } from 'express'
import { Express, CookieOptions } from 'express'
import mongoose from 'mongoose'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import { ModeType } from '../utils'
import { cookieOptions } from '../app'
import { DatabaseType, ModeType, ProtocolType } from '../utils'
export const configureExpressSession = (app: Express) => {
const { MODE } = process.env
const { MODE, DB_TYPE } = process.env
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
if (process.env.NODE_ENV !== 'test') {
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
collectionName: 'sessions'
})
if (DB_TYPE === DatabaseType.COSMOS_MONGODB) {
// COSMOS DB requires specific connection options (compatibility mode)
// See: https://www.npmjs.com/package/connect-mongo#set-the-compatibility-mode
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
autoRemove: 'interval'
})
} else {
store = MongoStore.create({
client: mongoose.connection!.getClient() as any
})
}
}
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: ALLOWED_DOMAIN?.trim() || undefined
}
app.use(
+1 -1
View File
@@ -23,7 +23,7 @@ export const configureLogger = (app: Express) => {
path: logsFolder
})
console.log('Writing Logs to :', path.join(logsFolder, filename))
process.logger.info('Writing Logs to :', path.join(logsFolder, filename))
options = { stream: accessLogStream }
}
+27 -23
View File
@@ -1,18 +1,21 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import csrf, { CookieOptions } from 'csurf'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import {
copySASjsCore,
createWeboutSasFile,
getFilesFolder,
getPackagesFolder,
getWebBuildFolder,
instantiateLogger,
loadAppStreamConfig,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders,
setupFilesFolder,
setupPackagesFolder,
setupUserAutoExec,
verifyEnvVariables
} from './utils'
import {
@@ -21,6 +24,7 @@ import {
configureLogger,
configureSecurity
} from './app-modules'
import { folderExists } from '@sasjs/utils'
dotenv.config()
@@ -30,25 +34,8 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
const { PROTOCOL } = process.env
export const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
/***********************************
* CSRF Protection *
***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions })
const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack)
process.logger.error(err.stack)
res.status(500).send('Something broke!')
}
@@ -77,8 +64,25 @@ export default setProcessVariables().then(async () => {
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
await setupFolders()
await copySASjsCore()
// Body parser is used for decoding the formdata on POST request.
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
app.use(express.urlencoded({ extended: true }))
await setupUserAutoExec()
if (!(await folderExists(getFilesFolder()))) await setupFilesFolder()
if (!(await folderExists(getPackagesFolder()))) await setupPackagesFolder()
const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos')
if (await folderExists(sasautosPath)) {
process.logger.warn(
`SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder`
)
} else {
await copySASjsCore()
await createWeboutSasFile()
}
// loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc
+109 -7
View File
@@ -1,12 +1,27 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import express from 'express'
import {
Security,
Route,
Tags,
Example,
Post,
Patch,
Request,
Body,
Query,
Hidden
} from 'tsoa'
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import {
generateAccessToken,
generateRefreshToken,
getTokensFromDB,
removeTokensInDB,
saveTokensInDB
} from '../utils'
import Client from '../model/Client'
import User from '../model/User'
@Route('SASjsApi/auth')
@Tags('Auth')
@@ -60,6 +75,18 @@ export class AuthController {
public async logout(@Query() @Hidden() data?: InfoJWT) {
return logout(data!)
}
/**
* @summary Update user's password.
*/
@Security('bearerAuth')
@Patch('updatePassword')
public async updatePassword(
@Request() req: express.Request,
@Body() body: UpdatePasswordPayload
) {
return updatePassword(req, body)
}
}
const token = async (data: any): Promise<TokenResponse> => {
@@ -73,8 +100,26 @@ const token = async (data: any): Promise<TokenResponse> => {
AuthController.deleteCode(userInfo.userId, clientId)
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
// get tokens from DB
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
if (existingTokens) {
return {
accessToken: existingTokens.accessToken,
refreshToken: existingTokens.refreshToken
}
}
const client = await Client.findOne({ clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
@@ -82,8 +127,17 @@ const token = async (data: any): Promise<TokenResponse> => {
}
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
const client = await Client.findOne({ clientId: userInfo.clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
await saveTokensInDB(
userInfo.userId,
@@ -99,6 +153,40 @@ const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
const updatePassword = async (
req: express.Request,
data: UpdatePasswordPayload
) => {
const { currentPassword, newPassword } = data
const userId = req.user?.userId
const dbUser = await User.findOne({ id: userId })
if (!dbUser)
throw {
code: 404,
message: `User not found!`
}
if (dbUser?.authProvider) {
throw {
code: 405,
message:
'Can not update password of user that is created by an external auth provider.'
}
}
const validPass = dbUser.comparePassword(currentPassword)
if (!validPass)
throw {
code: 403,
message: `Invalid current password!`
}
dbUser.password = User.hashPassword(newPassword)
dbUser.needsToUpdatePassword = false
await dbUser.save()
}
interface TokenPayload {
/**
* Client ID
@@ -125,6 +213,19 @@ interface TokenResponse {
refreshToken: string
}
interface UpdatePasswordPayload {
/**
* Current Password
* @example "currentPasswordString"
*/
currentPassword: string
/**
* New Password
* @example "newPassword"
*/
newPassword: string
}
const verifyAuthCode = async (
clientId: string,
code: string
@@ -133,9 +234,10 @@ const verifyAuthCode = async (
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
if (err) return resolve(undefined)
const payload = data as InfoJWT
const clientInfo: InfoJWT = {
clientId: data?.clientId,
userId: data?.userId
clientId: payload?.clientId,
userId: payload?.userId
}
if (clientInfo.clientId === clientId) {
return resolve(clientInfo)
+186
View File
@@ -0,0 +1,186 @@
import express from 'express'
import { Security, Route, Tags, Get, Post, Example } from 'tsoa'
import { LDAPClient, LDAPUser, LDAPGroup, AuthProviderType } from '../utils'
import { randomBytes } from 'crypto'
import User from '../model/User'
import Group from '../model/Group'
import Permission from '../model/Permission'
@Security('bearerAuth')
@Route('SASjsApi/authConfig')
@Tags('Auth_Config')
export class AuthConfigController {
/**
* @summary Gives the detail of Auth Mechanism.
*
*/
@Example({
ldap: {
LDAP_URL: 'ldaps://my.ldap.server:636',
LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron',
LDAP_BIND_PASSWORD: 'secret',
LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron',
LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'
}
})
@Get('/')
public getDetail() {
return getAuthConfigDetail()
}
/**
* @summary Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.
*
*/
@Example({
users: 5,
groups: 3
})
@Post('/synchroniseWithLDAP')
public async synchroniseWithLDAP() {
return synchroniseWithLDAP()
}
}
const synchroniseWithLDAP = async () => {
process.logger.info('Syncing LDAP with internal DB')
const permissions = await Permission.get({})
await Permission.deleteMany()
await User.deleteMany({ authProvider: AuthProviderType.LDAP })
await Group.deleteMany({ authProvider: AuthProviderType.LDAP })
const ldapClient = await LDAPClient.init()
process.logger.info('fetching LDAP users')
const users = await ldapClient.getAllLDAPUsers()
process.logger.info('inserting LDAP users to DB')
const existingUsers: string[] = []
const importedUsers: LDAPUser[] = []
for (const user of users) {
const usernameExists = await User.findOne({ username: user.username })
if (usernameExists) {
existingUsers.push(user.username)
continue
}
const hashPassword = User.hashPassword(randomBytes(64).toString('hex'))
await User.create({
displayName: user.displayName,
username: user.username,
password: hashPassword,
authProvider: AuthProviderType.LDAP,
needsToUpdatePassword: false
})
importedUsers.push(user)
}
if (existingUsers.length > 0) {
process.logger.info(
'Failed to insert following users as they already exist in DB:'
)
existingUsers.forEach((user) => process.logger.log(`* ${user}`))
}
process.logger.info('fetching LDAP groups')
const groups = await ldapClient.getAllLDAPGroups()
process.logger.info('inserting LDAP groups to DB')
const existingGroups: string[] = []
const importedGroups: LDAPGroup[] = []
for (const group of groups) {
const groupExists = await Group.findOne({ name: group.name })
if (groupExists) {
existingGroups.push(group.name)
continue
}
await Group.create({
name: group.name,
authProvider: AuthProviderType.LDAP
})
importedGroups.push(group)
}
if (existingGroups.length > 0) {
process.logger.info(
'Failed to insert following groups as they already exist in DB:'
)
existingGroups.forEach((group) => process.logger.log(`* ${group}`))
}
process.logger.info('associating users and groups')
for (const group of importedGroups) {
const dbGroup = await Group.findOne({ name: group.name })
if (dbGroup) {
for (const member of group.members) {
const user = importedUsers.find((user) => user.uid === member)
if (user) {
const dbUser = await User.findOne({ username: user.username })
if (dbUser) await dbGroup.addUser(dbUser)
}
}
}
}
process.logger.info('setting permissions')
for (const permission of permissions) {
const newPermission = new Permission({
path: permission.path,
type: permission.type,
setting: permission.setting
})
if (permission.user) {
const dbUser = await User.findOne({ username: permission.user.username })
if (dbUser) newPermission.user = dbUser._id
} else if (permission.group) {
const dbGroup = await Group.findOne({ name: permission.group.name })
if (dbGroup) newPermission.group = dbGroup._id
}
await newPermission.save()
}
process.logger.info('LDAP synchronization completed!')
return {
userCount: importedUsers.length,
groupCount: importedGroups.length
}
}
const getAuthConfigDetail = () => {
const { AUTH_PROVIDERS } = process.env
const returnObj: any = {}
if (AUTH_PROVIDERS === AuthProviderType.LDAP) {
const {
LDAP_URL,
LDAP_BIND_DN,
LDAP_BIND_PASSWORD,
LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN
} = process.env
returnObj.ldap = {
LDAP_URL: LDAP_URL ?? '',
LDAP_BIND_DN: LDAP_BIND_DN ?? '',
LDAP_BIND_PASSWORD: LDAP_BIND_PASSWORD ?? '',
LDAP_USERS_BASE_DN: LDAP_USERS_BASE_DN ?? '',
LDAP_GROUPS_BASE_DN: LDAP_GROUPS_BASE_DN ?? ''
}
}
return returnObj
}
+58 -8
View File
@@ -1,18 +1,27 @@
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
import { Security, Route, Tags, Example, Post, Body, Get } from 'tsoa'
import Client, { ClientPayload } from '../model/Client'
import Client, {
ClientPayload,
NUMBER_OF_SECONDS_IN_A_DAY
} from '../model/Client'
@Security('bearerAuth')
@Route('SASjsApi/client')
@Tags('Client')
export class ClientController {
/**
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
* @summary Admin only task. Create client with the following attributes:
* ClientId,
* ClientSecret,
* accessTokenExpiration (optional),
* refreshTokenExpiration (optional)
*
*/
@Example<ClientPayload>({
clientId: 'someFormattedClientID1234',
clientSecret: 'someRandomCryptoString'
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
})
@Post('/')
public async createClient(
@@ -20,10 +29,37 @@ export class ClientController {
): Promise<ClientPayload> {
return createClient(body)
}
/**
* @summary Admin only task. Returns the list of all the clients
*/
@Example<ClientPayload[]>([
{
clientId: 'someClientID1234',
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'someOtherClientID',
clientSecret: 'someOtherRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
])
@Get('/')
public async getAllClients(): Promise<ClientPayload[]> {
return getAllClients()
}
}
const createClient = async (data: any): Promise<ClientPayload> => {
const { clientId, clientSecret } = data
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
const {
clientId,
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
} = data
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId })
@@ -32,13 +68,27 @@ const createClient = async (data: any): Promise<ClientPayload> => {
// Create a new client
const client = new Client({
clientId,
clientSecret
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
})
const savedClient = await client.save()
return {
clientId: savedClient.clientId,
clientSecret: savedClient.clientSecret
clientSecret: savedClient.clientSecret,
accessTokenExpiration: savedClient.accessTokenExpiration,
refreshTokenExpiration: savedClient.refreshTokenExpiration
}
}
const getAllClients = async (): Promise<ClientPayload[]> => {
return Client.find({}).select({
_id: 0,
clientId: 1,
clientSecret: 1,
accessTokenExpiration: 1,
refreshTokenExpiration: 1
})
}
+106 -9
View File
@@ -1,34 +1,71 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController } from './internal'
import { ExecutionController, getSessionController } from './internal'
import {
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray,
RunTimeType
} from '../utils'
interface ExecuteCodePayload {
/**
* Code of program
* @example "* Code HERE;"
* The code to be executed
* @example "* Your Code HERE;"
*/
code: string
/**
* runtime for program
* The runtime for the code - eg SAS, JS, PY or R
* @example "js"
*/
runTime: RunTimeType
}
interface TriggerCodePayload {
/**
* The code to be executed
* @example "* Your Code HERE;"
*/
code: string
/**
* The runtime for the code - eg SAS, JS, PY or R
* @example "sas"
*/
runTime: RunTimeType
/**
* Amount of minutes after the completion of the job when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
}
interface TriggerCodeResponse {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store code outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
@Tags('Code')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
* Execute Code on the Specified Runtime
* @summary Run Code and Return Webout Content, Log and Print output
* The order of returned parts of the payload is:
* 1. Webout (if present)
* 2. Logs UUID (used as separator)
* 3. Log
* 4. Logs UUID (used as separator)
* 5. Print (if present and if the runtime is SAS)
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
*/
@Post('/execute')
public async executeCode(
@@ -37,6 +74,18 @@ export class CodeController {
): Promise<string | Buffer> {
return executeCode(request, body)
}
/**
* Trigger Code on the Specified Runtime
* @summary Triggers code and returns SessionId immediately - does not wait for job completion
*/
@Post('/trigger')
public async triggerCode(
@Request() request: express.Request,
@Body() body: TriggerCodePayload
): Promise<TriggerCodeResponse> {
return triggerCode(request, body)
}
}
const executeCode = async (
@@ -55,10 +104,58 @@ const executeCode = async (
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime
runTime: runTime,
includePrintOutput: true
})
return result
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err,
log: err?.log
}
}
}
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<TriggerCodeResponse> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
try {
// call executeProgram method of ExecutionController without awaiting
new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true,
session // session is provided
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
+23 -6
View File
@@ -12,6 +12,7 @@ import {
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import { AuthProviderType } from '../utils'
import { UserResponse } from './user'
export interface GroupResponse {
@@ -147,12 +148,14 @@ export class GroupController {
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return await group.remove()
}
}
@@ -248,6 +251,13 @@ const updateUsersListInGroup = async (
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
}
if (group.authProvider)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const user = await User.findOne({ id: userId })
if (!user)
throw {
@@ -256,6 +266,13 @@ const updateUsersListInGroup = async (
message: 'User not found.'
}
if (user.authProvider)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const updatedGroup =
action === 'addUser'
? await group.addUser(user)
+1
View File
@@ -1,4 +1,5 @@
export * from './auth'
export * from './authConfig'
export * from './client'
export * from './code'
export * from './drive'
+84 -22
View File
@@ -2,7 +2,7 @@ import path from 'path'
import fs from 'fs'
import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types'
import { PreProgramVars, Session, TreeNode, SessionState } from '../../types'
import {
extractHeaders,
getFilesFolder,
@@ -15,6 +15,24 @@ export interface ExecutionVars {
[key: string]: string | number | undefined
}
// Thrown when the session itself fails (e.g. SAS exits abnormally via
// %abort;). Carries the complete log - by the time this is thrown, the
// session's process has already exited, so the log file it wrote is final,
// not a partial/truncated snapshot.
export class SessionExecutionError extends Error {
constructor(
message: string,
public log?: string
) {
super(message)
// required for `instanceof` to work when compiling to ES5, since the
// default __extends helper does not preserve the prototype chain for
// classes extending built-ins like Error
Object.setPrototypeOf(this, SessionExecutionError.prototype)
}
}
export interface ExecuteReturnRaw {
httpHeaders: HTTPHeaders
result: string | Buffer
@@ -28,10 +46,12 @@ interface ExecuteFileParams {
returnJson?: boolean
session?: Session
runTime: RunTimeType
forceStringResult?: boolean
}
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string
includePrintOutput?: boolean
}
export class ExecutionController {
@@ -42,7 +62,8 @@ export class ExecutionController {
otherArgs,
returnJson,
session,
runTime
runTime,
forceStringResult
}: ExecuteFileParams) {
const program = await readFile(programPath)
@@ -53,7 +74,8 @@ export class ExecutionController {
otherArgs,
returnJson,
session,
runTime
runTime,
forceStringResult
})
}
@@ -63,14 +85,15 @@ export class ExecutionController {
vars,
otherArgs,
session: sessionByFileUpload,
runTime
runTime,
forceStringResult,
includePrintOutput
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime)
const session =
sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true
session.consumed = true
session.state = SessionState.running
const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt')
@@ -83,40 +106,79 @@ export class ExecutionController {
preProgramVariables?.httpHeaders.join('\n') ?? ''
)
await processProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
runTime,
logPath,
otherArgs
)
try {
await processProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
runTime,
logPath,
otherArgs
)
} catch (err: any) {
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
throw new SessionExecutionError(err.message, log)
}
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
const headersContent = (await fileExists(headersPath))
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
if (isDebugOn(vars)) {
httpHeaders['content-type'] = 'text/plain'
}
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
const webout = (await fileExists(weboutPath))
? fileResponse
? fileResponse && !forceStringResult
? await readFileBinary(weboutPath)
: await readFile(weboutPath)
: ''
// it should be deleted by scheduleSessionDestroy
session.inUse = false
//
// Guarded: for JS/PY/R, processProgram sets state to `failed` itself
// (without throwing) when the interpreter process exits non-zero - if
// we unconditionally set `completed` here we'd silently overwrite that,
// and anything downstream inspecting session.state (e.g.
// scheduleSessionDestroy's expiresAfterMins branch in Session.ts) would
// see a crashed session mis-reported as successful.
if ((session.state as SessionState) !== SessionState.failed) {
session.state = SessionState.completed
}
const resultParts = []
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty
if (webout && webout.length !== 0) resultParts.push(webout)
// INFO: log separator wraps the log from the beginning and the end
resultParts.push(process.logsUUID)
resultParts.push(log)
resultParts.push(process.logsUUID)
if (includePrintOutput && runTime === RunTimeType.SAS) {
const printOutputPath = path.join(session.path, 'output.lst')
const printOutput = (await fileExists(printOutputPath))
? await readFile(printOutputPath)
: ''
if (printOutput) resultParts.push(printOutput)
}
return {
httpHeaders,
result:
isDebugOn(vars) || session.crashed
? `${webout}\n${process.logsUUID}\n${log}`
isDebugOn(vars) || session.failureReason
? resultParts.join(`\n`)
: webout
}
}
@@ -2,11 +2,8 @@ import { Request, RequestHandler } from 'express'
import multer from 'multer'
import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.'
import {
executeProgramRawValidation,
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils'
import { SessionState } from '../../types'
export class FileUploadController {
private storage = multer.diskStorage({
@@ -56,9 +53,8 @@ export class FileUploadController {
}
const session = await sessionController.getSession()
// marking consumed true, so that it's not available
// as readySession for any other request
session.consumed = true
// change session state to 'running', so that it's not available for any other request
session.state = SessionState.running
req.sasjsSession = session
+117 -131
View File
@@ -1,8 +1,9 @@
import path from 'path'
import { Session } from '../../types'
import { Session, SessionState } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getPackagesFolder,
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath,
@@ -13,19 +14,47 @@ import {
createFile,
fileExists,
generateTimestamp,
readFile,
isWindows
readFile
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
abstract class SessionController {
export class SessionController {
protected sessions: Session[] = []
protected getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
this.sessions.filter(
(session: Session) => session.state === SessionState.pending
)
protected abstract createSession(): Promise<Session>
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
const session: Session = {
id: sessionId,
state: SessionState.pending,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
this.sessions.push(session)
return session
}
public async getSession() {
const readySessions = this.getReadySessions()
@@ -38,6 +67,10 @@ abstract class SessionController {
return session
}
public getSessionById(id: string) {
return this.sessions.find((session) => session.id === id)
}
}
export class SASSessionController extends SessionController {
@@ -55,15 +88,15 @@ export class SASSessionController extends SessionController {
const session: Session = {
id: sessionId,
ready: false,
inUse: false,
consumed: false,
completed: false,
state: SessionState.initialising,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8\n')
// we do not want to leave sessions running forever
// we clean them up after a predefined period, if unused
this.scheduleSessionDestroy(session)
@@ -73,7 +106,8 @@ export class SASSessionController extends SessionController {
// the autoexec file is executed on SAS startup
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
const contentForAutoExec = `/* compiled systemInit */
const contentForAutoExec = `filename packages "${getPackagesFolder()}";
/* compiled systemInit */
${compiledSystemInitContent}
/* autoexec */
${autoExecContent}`
@@ -101,23 +135,31 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
isWindows() ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
process.sasLoc!.endsWith('sas.exe') ? '-NOPRNGETLIST' : '',
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
])
.then(() => {
session.completed = true
console.log('session completed', session)
session.state = SessionState.completed
process.logger.info('session completed', session)
})
.catch((err) => {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
session.state = SessionState.failed
session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
})
// we have a triggered session - add to array
@@ -134,15 +176,22 @@ ${autoExecContent}`
const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {}
while (
(await fileExists(codeFilePath)) &&
session.state !== SessionState.failed
) {}
if (session.crashed)
console.log('session crashed! while waiting to be ready', session.crashed)
session.ready = true
if (session.state === SessionState.failed) {
process.logger.error(
'session crashed! while waiting to be ready',
session.failureReason
)
} else {
session.state = SessionState.pending
}
}
public async deleteSession(session: Session) {
private async deleteSession(session: Session) {
// remove the temporary files, to avoid buildup
await deleteFolder(session.path)
@@ -153,124 +202,54 @@ ${autoExecContent}`
}
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
if (session.inUse) {
// adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
setTimeout(
async () => {
if (session.state === SessionState.running) {
// adding 10 more minutes
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
}
}
this.scheduleSessionDestroy(session)
} else {
const { expiresAfterMins } = session
export class JSSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
// delay session destroy if expiresAfterMins present
if (expiresAfterMins && session.state !== SessionState.completed) {
// calculate session death time using expiresAfterMins
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) +
expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
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()
// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true
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: text/plain')
this.sessions.push(session)
return session
}
}
export class PythonSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
const session: Session = {
id: sessionId,
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: text/plain')
this.sessions.push(session)
return session
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}
},
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
)
}
}
export const getSessionController = (
runTime: RunTimeType
): SASSessionController | JSSessionController | PythonSessionController => {
): SessionController => {
if (runTime === RunTimeType.SAS) {
return getSASSessionController()
process.sasSessionController =
process.sasSessionController || new SASSessionController()
return process.sasSessionController
}
if (runTime === RunTimeType.JS) {
return getJSSessionController()
}
process.sessionController =
process.sessionController || new SessionController()
if (runTime === RunTimeType.PY) {
return getPythonSessionController()
}
throw new Error('No Runtime is configured')
}
const getSASSessionController = (): SASSessionController => {
if (process.sasSessionController) return process.sasSessionController
process.sasSessionController = new SASSessionController()
return process.sasSessionController
}
const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController()
return process.jsSessionController
}
const getPythonSessionController = (): PythonSessionController => {
if (process.pythonSessionController) return process.pythonSessionController
process.pythonSessionController = new PythonSessionController()
return process.pythonSessionController
return process.sessionController
}
const autoExecContent = `
@@ -281,9 +260,16 @@ data _null_;
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);
/* now wait for the real SYSIN */
slept=0;
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
/* now wait for the real SYSIN (location of code.sas) */
slept=0;fname='';
do until (slept>(60*15));
rc=filename(fname,getoption('SYSIN'));
if rc = 0 and fexist(fname) then do;
putlog fname=;
rc=filename(fname);
rc=sleep(0.01,1); /* wait just a little more */
stop;
end;
slept=slept+sleep(0.01,1);
end;
stop;
@@ -1,4 +1,4 @@
import { isWindows } from '@sasjs/utils'
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './'
@@ -15,19 +15,15 @@ export const createJSProgram = async (
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) =>
`${computed}const ${key} = '${vars[key]}';\n`,
`${computed}const ${key} = \`${vars[key]}\`;\n`,
''
)
const preProgramVarStatments = `
let _webout = '';
const weboutPath = '${
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
}';
const _SASJS_TOKENFILE = '${
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
}';
const _SASJS_WEBOUT_HEADERS = '${headersPath}';
const weboutPath = '${escapeWinSlashes(weboutPath)}';
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
const _SASJS_USERNAME = '${preProgramVariables?.username}';
const _SASJS_USERID = '${preProgramVariables?.userId}';
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
@@ -1,4 +1,4 @@
import { isWindows } from '@sasjs/utils'
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadPythonCode } from '../../utils'
import { ExecutionVars } from './'
@@ -19,14 +19,10 @@ export const createPythonProgram = async (
)
const preProgramVarStatments = `
_SASJS_SESSION_PATH = '${
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
}';
_WEBOUT = '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
_SASJS_WEBOUT_HEADERS = '${headersPath}';
_SASJS_TOKENFILE = '${
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
}';
_SASJS_SESSION_PATH = '${escapeWinSlashes(session.path)}';
_WEBOUT = '${escapeWinSlashes(weboutPath)}';
_SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
_SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
_SASJS_USERNAME = '${preProgramVariables?.username}';
_SASJS_USERID = '${preProgramVariables?.userId}';
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
@@ -0,0 +1,64 @@
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadRCode } from '../../utils'
import { ExecutionVars } from '.'
export const createRProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => `${computed}.${key} <- '${vars[key]}'\n`,
''
)
const preProgramVarStatments = `
._SASJS_SESSION_PATH <- '${escapeWinSlashes(session.path)}';
._WEBOUT <- '${escapeWinSlashes(weboutPath)}';
._SASJS_WEBOUT_HEADERS <- '${escapeWinSlashes(headersPath)}';
._SASJS_TOKENFILE <- '${escapeWinSlashes(tokenFile)}';
._SASJS_USERNAME <- '${preProgramVariables?.username}';
._SASJS_USERID <- '${preProgramVariables?.userId}';
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
._METAPERSON <- ._SASJS_DISPLAYNAME;
._METAUSER <- ._SASJS_USERNAME;
SASJSPROCESSMODE <- 'Stored Program';
`
const requiredModules = ``
program = `
# runtime vars
${varStatments}
# dynamic user-provided vars
${preProgramVarStatments}
# change working directory to session folder
setwd(._SASJS_SESSION_PATH)
# actual job code
${program}
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadRCode = await generateFileUploadRCode(
otherArgs.filesNamesMap,
session.path
)
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadRCode.length > 0) {
program = `${uploadRCode}\n` + program
}
}
return requiredModules + program
}
@@ -8,6 +8,7 @@ export const createSASProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
@@ -23,7 +24,7 @@ export const createSASProgram = async (
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _sasjs_webout_headers=%sysfunc(pathname(work))/../stpsrv_header.txt;
%let _sasjs_webout_headers=${headersPath};
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
@@ -39,8 +40,6 @@ export const createSASProgram = async (
%mend;
%_sasjs_server_init()
proc printto print="%sysfunc(getoption(log))";
run;
`
program = `
+1
View File
@@ -5,4 +5,5 @@ export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './createPythonProgram'
export * from './createRProgram'
export * from './processProgram'
+123 -80
View File
@@ -1,15 +1,16 @@
import path from 'path'
import fs from 'fs'
import { execFileSync } from 'child_process'
import { WriteStream, createWriteStream } from 'fs'
import { execFile } from 'child_process'
import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { PreProgramVars, Session, SessionState } from '../../types'
import { RunTimeType } from '../../utils'
import {
ExecutionVars,
createSASProgram,
createJSProgram,
createPythonProgram
createPythonProgram,
createRProgram
} from './'
export const processProgram = async (
@@ -24,87 +25,14 @@ export const processProgram = async (
logPath: string,
otherArgs?: any
) => {
if (runTime === RunTimeType.JS) {
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.js')
try {
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = fs.createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
execFileSync(process.nodeLoc!, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.js program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else if (runTime === RunTimeType.PY) {
program = await createPythonProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.py')
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.pythonLoc!, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.py program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else {
if (runTime === RunTimeType.SAS) {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
@@ -121,10 +49,125 @@ export const processProgram = async (
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status
while (!session.completed) {
while (session.state !== SessionState.completed) {
if (session.state === SessionState.failed) {
throw new Error(session.failureReason || 'SAS session failed')
}
await delay(50)
}
} else {
let codePath: string
let executablePath: string
switch (runTime) {
case RunTimeType.JS:
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
codePath = path.join(session.path, 'code.js')
executablePath = process.nodeLoc!
break
case RunTimeType.PY:
program = await createPythonProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
codePath = path.join(session.path, 'code.py')
executablePath = process.pythonLoc!
break
case RunTimeType.R:
program = await createRProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
codePath = path.join(session.path, 'code.r')
executablePath = process.rLoc!
break
default:
throw new Error('Invalid runtime!')
}
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
await execFilePromise(executablePath, [codePath], writeStream)
.then(() => {
session.state = SessionState.completed
process.logger.info('session completed', session)
})
.catch((err) => {
session.state = SessionState.failed
session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
})
// copy the code file to log and end write stream
writeStream.end(program)
}
}
/**
* Promisified child_process.execFile
*
* @param file - The name or path of the executable file to run.
* @param args - List of string arguments.
* @param writeStream - Child process stdout and stderr will be piped to it.
*
* @returns {Promise<{ stdout: string, stderr: string }>}
*/
const execFilePromise = (
file: string,
args: string[],
writeStream: WriteStream
): Promise<{ stdout: string; stderr: string }> => {
return new Promise((resolve, reject) => {
const child = execFile(file, args, (err, stdout, stderr) => {
if (err) reject(err)
resolve({ stdout, stderr })
})
child.stdout?.on('data', (data) => {
writeStream.write(data)
})
child.stderr?.on('data', (data) => {
writeStream.write(data)
})
})
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
@@ -0,0 +1,117 @@
import path from 'path'
import os from 'os'
import { createFile, deleteFolder, generateTimestamp } from '@sasjs/utils'
import * as ProcessProgramModule from '../processProgram'
import { ExecutionController, SessionExecutionError } from '../Execution'
import { Session, SessionState, PreProgramVars } from '../../../types'
import { RunTimeType } from '../../../utils'
const preProgramVariables: PreProgramVars = {
username: 'testUser',
userId: 1,
displayName: 'Test User',
serverUrl: 'http://localhost:5000',
httpHeaders: []
}
describe('ExecutionController.executeProgram', () => {
let session: Session
beforeEach(() => {
const sessionId = `test-session-${generateTimestamp()}`
session = {
id: sessionId,
state: SessionState.pending,
creationTimeStamp: '0',
deathTimeStamp: '0',
path: path.join(os.tmpdir(), sessionId)
}
})
afterEach(async () => {
jest.restoreAllMocks()
await deleteFolder(session.path)
})
// Regression coverage: for JS/PY/R, processProgram sets session.state to
// `failed` itself (without throwing) on a non-zero interpreter exit.
// ExecutionController.executeProgram used to unconditionally overwrite
// that back to `completed` right after processProgram returned, silently
// losing the failure for anything downstream that inspects session.state.
describe('JS/PY/R failure path', () => {
it('does not overwrite a failed session state back to completed', async () => {
// mirrors processProgram's real JS/PY/R branch on a non-zero exit:
// it sets state/failureReason itself and resolves - it does not throw
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(async () => {
session.state = SessionState.failed
session.failureReason = 'Error: process exited with code 1'
})
const controller = new ExecutionController()
await controller.executeProgram({
program: 'throw new Error("boom")',
preProgramVariables,
vars: {},
session,
runTime: RunTimeType.JS
})
expect(session.state).toBe(SessionState.failed)
})
it('still marks a genuinely successful session as completed', async () => {
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(async () => {
session.state = SessionState.completed
})
const controller = new ExecutionController()
await controller.executeProgram({
program: 'console.log("hello")',
preProgramVariables,
vars: {},
session,
runTime: RunTimeType.JS
})
expect(session.state).toBe(SessionState.completed)
})
})
describe('SAS failure path', () => {
it('throws a SessionExecutionError carrying the complete log when the session fails', async () => {
const logPath = path.join(session.path, 'log.log')
const logContent =
'NOTE: SAS session\nERROR: SAS session terminated. See log for details.\n'
await createFile(logPath, logContent)
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(async () => {
throw new Error('ERROR: SAS session terminated. See log for details.')
})
const controller = new ExecutionController()
const resultPromise = controller.executeProgram({
program: '%abort;',
preProgramVariables,
vars: {},
session,
runTime: RunTimeType.SAS
})
await expect(resultPromise).rejects.toBeInstanceOf(SessionExecutionError)
await expect(resultPromise).rejects.toMatchObject({
log: logContent,
message: expect.stringContaining('SAS session terminated')
})
})
})
})
@@ -0,0 +1,109 @@
import path from 'path'
import os from 'os'
import { createFile, deleteFolder, generateTimestamp } from '@sasjs/utils'
import { processProgram } from '../processProgram'
import { Session, SessionState, PreProgramVars } from '../../../types'
import {
RunTimeType,
getSessionsFolder,
generateUniqueFileName
} from '../../../utils'
const preProgramVariables: PreProgramVars = {
username: 'testUser',
userId: 1,
displayName: 'Test User',
serverUrl: 'http://localhost:5000',
httpHeaders: []
}
const makeSession = (): Session => {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
return {
id: sessionId,
state: SessionState.running,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
}
describe('processProgram (SAS runtime)', () => {
let session: Session
let logPath: string
let weboutPath: string
let headersPath: string
let tokenFile: string
beforeAll(() => {
const root = path.join(
os.tmpdir(),
`sasjs-processProgram-spec-${generateTimestamp()}`
)
process.sasjsRoot = root
process.driveLoc = path.join(root, 'drive')
})
beforeEach(async () => {
session = makeSession()
logPath = path.join(session.path, 'log.log')
weboutPath = path.join(session.path, 'webout.txt')
headersPath = path.join(session.path, 'stpsrv_header.txt')
tokenFile = path.join(session.path, 'reqHeaders.txt')
await createFile(weboutPath, '')
})
afterEach(async () => {
await deleteFolder(session.path)
})
it('rejects instead of hanging when the session fails (e.g. %abort;)', async () => {
setTimeout(() => {
session.state = SessionState.failed
session.failureReason =
'ERROR: SAS session terminated. See log for details.'
}, 100)
await expect(
processProgram(
'%abort;',
preProgramVariables,
{},
session,
weboutPath,
headersPath,
tokenFile,
RunTimeType.SAS,
logPath
)
).rejects.toThrow(/SAS session terminated/)
}, 3000)
it('resolves without throwing when the session completes normally', async () => {
setTimeout(() => {
session.state = SessionState.completed
}, 100)
await expect(
processProgram(
'%put hello world;',
preProgramVariables,
{},
session,
weboutPath,
headersPath,
tokenFile,
RunTimeType.SAS,
logPath
)
).resolves.toBeUndefined()
}, 3000)
})
+283
View File
@@ -0,0 +1,283 @@
import { readFile } from '@sasjs/utils'
import express from 'express'
import path from 'path'
import { Request, Post, Get } from 'tsoa'
import dotenv from 'dotenv'
import { ExecutionController } from './internal'
import {
getPreProgramVariables,
getRunTimeAndFilePath,
makeFilesNamesMap
} from '../utils'
import { MulterFile } from '../types/Upload'
dotenv.config()
export interface Sas9Response {
content: string
redirect?: string
error?: boolean
}
export interface MockFileRead {
content: string
error?: boolean
}
export class MockSas9Controller {
private loggedIn: string | undefined
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
@Get('/SASStoredProcess')
public async sasStoredProcess(
@Request() req: express.Request
): Promise<Sas9Response> {
const username = req.query._username?.toString() || undefined
const password = req.query._password?.toString() || undefined
if (username && password) this.loggedIn = req.body.username
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
let program = req.query._program?.toString() || undefined
const filePath: string[] = program
? program.replace('/', '').split('/')
: ['generic', 'sas-stored-process']
if (program) {
return await getMockResponseFromFile([
process.cwd(),
this.mocksPath,
'sas9',
...filePath
])
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
...filePath
])
}
@Get('/SASStoredProcess/do')
public async sasStoredProcessDoGet(
@Request() req: express.Request
): Promise<Sas9Response> {
const username = req.query._username?.toString() || undefined
const password = req.query._password?.toString() || undefined
if (username && password) this.loggedIn = username
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
const program = req.query._program ?? req.body?._program
const filePath: string[] = ['generic', 'sas-stored-process']
if (program) {
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
const otherArgs = {}
try {
const { codePath, runTime } = await getRunTimeAndFilePath(
program + '.js'
)
const result = await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: vars,
otherArgs: otherArgs,
runTime,
forceStringResult: true
})
return {
content: result.result as string
}
} catch (err) {
process.logger.error('err', err)
}
return {
content: 'No webout returned.'
}
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
...filePath
])
}
@Post('/SASStoredProcess/do/')
public async sasStoredProcessDoPost(
@Request() req: express.Request
): Promise<Sas9Response> {
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
if (this.isPublicAccount()) {
return {
content: '',
redirect: '/SASLogon/Login'
}
}
const program = req.query._program ?? req.body?._program
const vars = {
...req.query,
...req.body,
_requestMethod: req.method,
_driveLoc: process.driveLoc
}
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
const otherArgs = { filesNamesMap: filesNamesMap }
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
try {
const result = await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: vars,
otherArgs: otherArgs,
runTime,
session: req.sasjsSession,
forceStringResult: true
})
return {
content: result.result as string
}
} catch (err) {
process.logger.error('err', err)
}
return {
content: 'No webout returned.'
}
}
@Get('/SASLogon/login')
public async loginGet(): Promise<Sas9Response> {
if (this.loggedIn) {
if (this.isPublicAccount()) {
return {
content: '',
redirect: '/SASStoredProcess/Logoff?publicDenied=true'
}
} else {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'logged-in'
])
}
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'login'
])
}
@Post('/SASLogon/login')
public async loginPost(req: express.Request): Promise<Sas9Response> {
if (req.body.lt && req.body.lt !== 'validtoken')
return {
content: '',
redirect: '/SASLogon/login'
}
this.loggedIn = req.body.username
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'logged-in'
])
}
@Get('/SASLogon/logout')
public async logout(req: express.Request): Promise<Sas9Response> {
this.loggedIn = undefined
if (req.query.publicDenied === 'true') {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'public-access-denied'
])
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'logged-out'
])
}
@Get('/SASStoredProcess/Logoff') //publicDenied=true
public async logoff(req: express.Request): Promise<Sas9Response> {
const params = req.query.publicDenied
? `?publicDenied=${req.query.publicDenied}`
: ''
return {
content: '',
redirect: '/SASLogon/logout' + params
}
}
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
}
const getMockResponseFromFile = async (
filePath: string[]
): Promise<MockFileRead> => {
const filePathParsed = path.join(...filePath)
let error: boolean = false
let file = await readFile(filePathParsed).catch((err: any) => {
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
process.logger.error(errMsg)
error = true
return errMsg
})
return {
content: file,
error: error
}
}
+41 -2
View File
@@ -1,6 +1,12 @@
import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user'
import { getSessionController } from './internal'
import { SessionState } from '../types'
interface SessionResponse extends UserResponse {
needsToUpdatePassword: boolean
}
@Security('bearerAuth')
@Route('SASjsApi/session')
@@ -19,14 +25,47 @@ export class SessionController {
@Get('/')
public async session(
@Request() request: express.Request
): Promise<UserResponse> {
): Promise<SessionResponse> {
return session(request)
}
/**
* The polling endpoint is currently implemented for single-server deployments only.<br>
* Load balanced / grid topologies will be supported in a future release.<br>
* If your site requires this, please reach out to SASjs Support.
* @summary Get session state (initialising, pending, running, completed, failed).
* @example completed
*/
@Get('/:sessionId/state')
public async sessionState(sessionId: string): Promise<SessionState> {
return sessionState(sessionId)
}
}
const session = (req: express.Request) => ({
id: req.user!.userId,
username: req.user!.username,
displayName: req.user!.displayName,
isAdmin: req.user!.isAdmin
isAdmin: req.user!.isAdmin,
needsToUpdatePassword: req.user!.needsToUpdatePassword
})
const sessionState = (sessionId: string): SessionState => {
for (let runTime of process.runTimes) {
// get session controller for each available runTime
const sessionController = getSessionController(runTime)
// get session by sessionId
const session = sessionController.getSessionById(sessionId)
// return session state if session was found
if (session) {
return session.state
}
}
throw {
code: 404,
message: `Session with ID '${sessionId}' was not found.`
}
}
+127 -18
View File
@@ -1,10 +1,12 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { ExecutionController, ExecutionVars } from './internal'
import {
ExecutionController,
ExecutionVars,
getSessionController
} from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
LogLine,
makeFilesNamesMap,
getRunTimeAndFilePath
} from '../utils'
@@ -18,46 +20,81 @@ interface ExecutePostRequestPayload {
_program?: string
}
interface TriggerProgramPayload {
/**
* Location of SAS program.
* @example "/Public/somefolder/some.file"
*/
_program: string
/**
* Amount of minutes after the completion of the program when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
/**
* Query param for setting debug mode.
*/
_debug?: number
}
interface TriggerProgramResponse {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store program outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth')
@Route('SASjsApi/stp')
@Tags('STP')
export class STPController {
/**
* Trigger a SAS or JS program using the _program URL parameter.
* Trigger a Stored Program using the _program URL parameter.
*
* Accepts URL parameters and file uploads. For more details, see docs:
* Accepts additional URL parameters (converted to session variables)
* and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* @summary Execute a Stored Program, returns raw _webout content.
* @param _program Location of SAS or JS code
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of Stored Program in SASjs Drive.
* @param _debug Optional query param for setting debug mode (returns the session log in the response body).
* @example _program "/Projects/myApp/some/program"
* @example _debug 131
*/
@Get('/execute')
public async executeGetRequest(
@Request() request: express.Request,
@Query() _program: string
@Query() _program: string,
@Query() _debug?: number
): Promise<string | Buffer> {
const vars = request.query as ExecutionVars
let vars = request.query as ExecutionVars
if (_debug) {
vars = {
...vars,
_debug
}
}
return execute(request, _program, vars)
}
/**
* Trigger a SAS or JS program using the _program URL parameter.
* Trigger a Stored Program using the _program URL parameter.
*
* Accepts URL parameters and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* The response will be a JSON object with the following root attributes:
* log, webout, headers.
*
* The webout attribute will be nested JSON ONLY if the response-header
* contains a content-type of application/json AND it is valid JSON.
* Otherwise it will be a stringified version of the webout content.
*
* @summary Execute a Stored Program, return a JSON object
* @param _program Location of SAS or JS code
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive
* @example _program "/Projects/myApp/some/program"
*/
@Post('/execute')
@@ -75,6 +112,26 @@ export class STPController {
return execute(request, program!, vars, otherArgs)
}
/**
* Trigger Program on the Specified Runtime.
* @summary Triggers program and returns SessionId immediately - does not wait for program completion.
* @param _program Location of code in SASjs Drive.
* @param expiresAfterMins Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.
* @param _debug Optional query param for setting debug mode.
* @example _program "/Projects/myApp/some/program"
* @example _debug 131
* @example expiresAfterMins 15
*/
@Post('/trigger')
public async triggerProgram(
@Request() request: express.Request,
@Query() _program: string,
@Query() _debug?: number,
@Query() expiresAfterMins?: number
): Promise<TriggerProgramResponse> {
return triggerProgram(request, { _program, _debug, expiresAfterMins })
}
}
const execute = async (
@@ -97,11 +154,63 @@ const execute = async (
}
)
req.res?.header(httpHeaders)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err,
log: err?.log
}
}
}
const triggerProgram = async (
req: express.Request,
{ _program, _debug, expiresAfterMins }: TriggerProgramPayload
): Promise<TriggerProgramResponse> => {
try {
// put _program query param into vars object
const vars: { [key: string]: string | number } = { _program }
// if present add _debug query param to vars object
if (_debug) {
vars._debug = _debug
}
// get code path and runTime
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
// call executeFile method of ExecutionController without awaiting
new ExecutionController().executeFile({
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars,
session
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
+63 -9
View File
@@ -17,8 +17,13 @@ import {
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import { GroupResponse } from './group'
import {
getUserAutoExec,
updateUserAutoExec,
ModeType,
ALL_USERS_GROUP
} from '../utils'
import { GroupController, GroupResponse } from './group'
export interface UserResponse {
id: number
@@ -211,7 +216,11 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) throw new Error('Username already exists.')
if (usernameExist)
throw {
code: 409,
message: 'Username already exists.'
}
// Hash passwords
const hashPassword = User.hashPassword(password)
@@ -228,6 +237,15 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const savedUser = await user.save()
const groupController = new GroupController()
const allUsersGroup = await groupController
.getGroupByGroupName(ALL_USERS_GROUP.name)
.catch(() => {})
if (allUsersGroup) {
await groupController.addUserToGroup(allUsersGroup.groupId, savedUser.id)
}
return {
id: savedUser.id,
displayName: savedUser.displayName,
@@ -255,7 +273,11 @@ const getUser = async (
'groupId name description -_id'
)) as unknown as UserDetailsResponse
if (!user) throw new Error('User is not found.')
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
return {
id: user.id,
@@ -263,7 +285,7 @@ const getUser = async (
username: user.username,
isActive: user.isActive,
isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
autoExec: getAutoExec ? (user.autoExec ?? '') : undefined,
groups: user.groups
}
}
@@ -284,6 +306,24 @@ const updateUser = async (
const params: any = { displayName, isAdmin, isActive, autoExec }
const user = await User.findOne(findBy)
if (username && username !== user?.username && user?.authProvider) {
throw {
code: 405,
message:
'Can not update username of user that is created by an external auth provider.'
}
}
if (displayName && displayName !== user?.displayName && user?.authProvider) {
throw {
code: 405,
message:
'Can not update display name of user that is created by an external auth provider.'
}
}
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
@@ -292,7 +332,10 @@ const updateUser = async (
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw new Error('Username already exists.')
throw {
code: 409,
message: 'Username already exists.'
}
}
params.username = username
}
@@ -305,7 +348,10 @@ const updateUser = async (
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
if (!updatedUser)
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
throw {
code: 404,
message: `Unable to find user with ${findBy.id || findBy.username}`
}
return {
id: updatedUser.id,
@@ -332,11 +378,19 @@ const deleteUser = async (
{ password }: { password?: string }
) => {
const user = await User.findOne(findBy)
if (!user) throw new Error('User is not found.')
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
if (!isAdmin) {
const validPass = user.comparePassword(password!)
if (!validPass) throw new Error('Invalid password.')
if (!validPass)
throw {
code: 401,
message: 'Invalid password.'
}
}
await User.deleteOne(findBy)
+60 -7
View File
@@ -1,11 +1,17 @@
import path from 'path'
import express from 'express'
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
import { readFile } from '@sasjs/utils'
import { readFile, convertSecondsToHms } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import { getWebBuildFolder, generateAuthCode } from '../utils'
import {
getWebBuildFolder,
generateAuthCode,
RateLimiter,
AuthProviderType,
LDAPClient
} from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@@ -78,10 +84,40 @@ const login = async (
) => {
// Authenticate User
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
let validPass = false
if (user) {
if (
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
user.authProvider === AuthProviderType.LDAP
) {
const ldapClient = await LDAPClient.init()
validPass = await ldapClient
.verifyUser(username, password)
.catch(() => false)
} else {
validPass = user.comparePassword(password)
}
}
// code to prevent brute force attack
const rateLimiter = RateLimiter.getInstance()
if (!validPass) {
const retrySecs = await rateLimiter.consume(
req.ip || 'unknown',
user?.username
)
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
}
if (!user) throw errors.userNotFound
if (!validPass) throw errors.invalidPassword
// Reset on successful authorization
rateLimiter.resetOnSuccess(req.ip || 'unknown', user.username)
req.session.loggedIn = true
req.session.user = {
@@ -91,7 +127,8 @@ const login = async (
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive,
autoExec: user.autoExec
autoExec: user.autoExec,
needsToUpdatePassword: user.needsToUpdatePassword
}
return {
@@ -100,7 +137,8 @@ const login = async (
id: user.id,
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin
isAdmin: user.isAdmin,
needsToUpdatePassword: user.needsToUpdatePassword
}
}
}
@@ -157,3 +195,18 @@ interface AuthorizeResponse {
*/
code: string
}
const errors = {
invalidPassword: {
code: 401,
message: 'Invalid Password.'
},
userNotFound: {
code: 401,
message: 'Username is not found.'
},
tooManyRequests: (seconds: number) => ({
code: 429,
message: `Too Many Requests! Retry after ${convertSecondsToHms(seconds)}`
})
}
+6 -5
View File
@@ -1,6 +1,6 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import { csrfProtection } from './'
import {
fetchLatestAutoExec,
ModeType,
@@ -37,10 +37,10 @@ export const authenticateAccessToken: RequestHandler = async (
if (user.isActive) {
req.user = user
return csrfProtection(req, res, nextFunction)
} else return res.sendStatus(401)
} else return res.status(401).send('Unauthorized')
}
}
return res.sendStatus(401)
return res.status(401).send('Unauthorized')
}
await authenticateToken(
@@ -81,7 +81,8 @@ const authenticateToken = async (
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',
isAdmin: true,
isActive: true
isActive: true,
needsToUpdatePassword: false
}
req.accessToken = 'desktopModeAccessToken'
return next()
@@ -117,6 +118,6 @@ const authenticateToken = async (
return next()
}
res.sendStatus(401)
res.status(401).send('Unauthorized')
}
}
+42 -6
View File
@@ -5,14 +5,12 @@ import {
PermissionSettingForRoute,
PermissionType
} from '../controllers/permission'
import { getPath, isPublicRoute } from '../utils'
import { getPath, isPublicRoute, TopLevelRoutes } from '../utils'
export const authorize: RequestHandler = async (req, res, next) => {
const { user } = req
if (!user) {
return res.sendStatus(401)
}
if (!user) return res.sendStatus(401)
// no need to check for permissions when user is admin
if (user.isAdmin) return next()
@@ -24,6 +22,9 @@ export const authorize: RequestHandler = async (req, res, next) => {
if (!dbUser) return res.sendStatus(401)
const path = getPath(req)
const { baseUrl } = req
const topLevelRoute =
TopLevelRoutes.find((route) => baseUrl.startsWith(route)) || baseUrl
// find permission w.r.t user
const permission = await Permission.findOne({
@@ -37,6 +38,21 @@ export const authorize: RequestHandler = async (req, res, next) => {
else return res.sendStatus(401)
}
// find permission w.r.t user on top level
const topLevelPermission = await Permission.findOne({
path: topLevelRoute,
type: PermissionType.route,
user: dbUser._id
})
if (topLevelPermission) {
if (topLevelPermission.setting === PermissionSettingForRoute.grant)
return next()
else return res.sendStatus(401)
}
let isPermissionDenied = false
// find permission w.r.t user's groups
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({
@@ -44,8 +60,28 @@ export const authorize: RequestHandler = async (req, res, next) => {
type: PermissionType.route,
group
})
if (groupPermission?.setting === PermissionSettingForRoute.grant)
return next()
if (groupPermission) {
if (groupPermission.setting === PermissionSettingForRoute.grant) {
return next()
} else {
isPermissionDenied = true
}
}
}
if (!isPermissionDenied) {
// find permission w.r.t user's groups on top level
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({
path: topLevelRoute,
type: PermissionType.route,
group
})
if (groupPermission?.setting === PermissionSettingForRoute.grant)
return next()
}
}
return res.sendStatus(401)
}
@@ -0,0 +1,22 @@
import { RequestHandler } from 'express'
import { convertSecondsToHms } from '@sasjs/utils'
import { RateLimiter } from '../utils'
export const bruteForceProtection: RequestHandler = async (req, res, next) => {
const ip = req.ip || 'unknown'
const username = req.body.username
const rateLimiter = RateLimiter.getInstance()
const retrySecs = await rateLimiter.check(ip, username)
if (retrySecs > 0) {
res
.status(429)
.send(`Too Many Requests! Retry after ${convertSecondsToHms(retrySecs)}`)
return
}
next()
}
+32
View File
@@ -0,0 +1,32 @@
import { RequestHandler } from 'express'
import csrf from 'csrf'
const csrfTokens = new csrf()
const secret = csrfTokens.secretSync()
export const generateCSRFToken = () => csrfTokens.create(secret)
export const csrfProtection: RequestHandler = (req, res, next) => {
if (req.method === 'GET') return next()
// Reads the token from the following locations, in order:
// req.body.csrf_token - typically generated by the body-parser module.
// req.query.csrf_token - a built-in from Express.js to read from the URL query string.
// req.headers['csrf-token'] - the CSRF-Token HTTP request header.
// req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
// req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
// req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.
const token =
req.body?.csrf_token ||
req.query?.csrf_token ||
req.headers['csrf-token'] ||
req.headers['xsrf-token'] ||
req.headers['x-csrf-token'] ||
req.headers['x-xsrf-token']
if (!csrfTokens.verify(secret, token)) {
return res.status(400).send('Invalid CSRF token!')
}
next()
}
+2 -1
View File
@@ -33,5 +33,6 @@ export const desktopUser: RequestUser = {
username: userInfo().username,
displayName: userInfo().username,
isAdmin: true,
isActive: true
isActive: true,
needsToUpdatePassword: false
}
+3 -1
View File
@@ -1,5 +1,7 @@
export * from './authenticateToken'
export * from './authorize'
export * from './csrfProtection'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'
export * from './authorize'
export * from './bruteForceProtection'
+19
View File
@@ -1,5 +1,6 @@
import mongoose, { Schema } from 'mongoose'
export const NUMBER_OF_SECONDS_IN_A_DAY = 86400
export interface ClientPayload {
/**
* Client ID
@@ -11,6 +12,16 @@ export interface ClientPayload {
* @example "someRandomCryptoString"
*/
clientSecret: string
/**
* Number of seconds after which access token will expire. Default is 86400 (1 day)
* @example 86400
*/
accessTokenExpiration?: number
/**
* Number of seconds after which access token will expire. Default is 2592000 (30 days)
* @example 2592000
*/
refreshTokenExpiration?: number
}
const ClientSchema = new Schema<ClientPayload>({
@@ -21,6 +32,14 @@ const ClientSchema = new Schema<ClientPayload>({
clientSecret: {
type: String,
required: true
},
accessTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY
},
refreshTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
})
+15
View File
@@ -0,0 +1,15 @@
import mongoose, { Schema } from 'mongoose'
const CounterSchema = new Schema({
id: {
type: String,
required: true,
unique: true
},
seq: {
type: Number,
required: true
}
})
export default mongoose.model('Counter', CounterSchema)
+18 -5
View File
@@ -1,7 +1,7 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
import { Schema, model, Document, Model } from 'mongoose'
import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import { AuthProviderType, getSequenceNextValue } from '../utils'
export const PUBLIC_GROUP_NAME = 'Public'
@@ -27,6 +27,7 @@ interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean
users: Schema.Types.ObjectId[]
authProvider?: AuthProviderType
}
interface IGroup extends IGroupDocument {
@@ -42,10 +43,18 @@ const groupSchema = new Schema<IGroupDocument>({
required: true,
unique: true
},
groupId: {
type: Number,
unique: true
},
description: {
type: String,
default: 'Group description.'
},
authProvider: {
type: String,
enum: AuthProviderType
},
isActive: {
type: Boolean,
default: true
@@ -53,9 +62,13 @@ const groupSchema = new Schema<IGroupDocument>({
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks
groupSchema.pre('save', async function () {
if (this.isNew) {
this.groupId = await getSequenceNextValue('groupId')
}
})
groupSchema.post('save', function (group: IGroup, next: Function) {
group.populate('users', 'id username displayName -_id').then(function () {
next()
@@ -63,7 +76,7 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
})
// pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function () {
groupSchema.pre('remove', async function (this: IGroupDocument) {
const userIds = this.users
await Promise.all(
userIds.map(async (userId) => {
+12 -3
View File
@@ -1,6 +1,6 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import { Schema, model, Document, Model } from 'mongoose'
import { PermissionDetailsResponse } from '../controllers'
import { getSequenceNextValue } from '../utils'
interface GetPermissionBy {
user?: Schema.Types.ObjectId
@@ -23,6 +23,10 @@ interface IPermissionModel extends Model<IPermission> {
}
const permissionSchema = new Schema<IPermissionDocument>({
permissionId: {
type: Number,
unique: true
},
path: {
type: String,
required: true
@@ -39,7 +43,12 @@ const permissionSchema = new Schema<IPermissionDocument>({
group: { type: Schema.Types.ObjectId, ref: 'Group' }
})
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
// Hooks
permissionSchema.pre('save', async function () {
if (this.isNew) {
this.permissionId = await getSequenceNextValue('permissionId')
}
})
// Static Methods
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
+25 -3
View File
@@ -1,6 +1,6 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import { Schema, model, Document, Model } from 'mongoose'
import bcrypt from 'bcryptjs'
import { AuthProviderType, getSequenceNextValue } from '../utils'
export interface UserPayload {
/**
@@ -39,9 +39,11 @@ interface IUserDocument extends UserPayload, Document {
id: number
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType
}
export interface IUser extends IUserDocument {
@@ -63,10 +65,18 @@ const userSchema = new Schema<IUserDocument>({
required: true,
unique: true
},
id: {
type: Number,
unique: true
},
password: {
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType
},
isAdmin: {
type: Boolean,
default: false
@@ -75,6 +85,10 @@ const userSchema = new Schema<IUserDocument>({
type: Boolean,
default: true
},
needsToUpdatePassword: {
type: Boolean,
default: true
},
autoExec: {
type: String
},
@@ -96,7 +110,15 @@ const userSchema = new Schema<IUserDocument>({
}
]
})
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
// Hooks
userSchema.pre('save', async function (next) {
if (this.isNew) {
this.id = await getSequenceNextValue('id')
}
next()
})
// Static Methods
userSchema.static('hashPassword', (password: string): string => {
+17 -1
View File
@@ -7,12 +7,28 @@ import {
authenticateRefreshToken
} from '../../middlewares'
import { authorizeValidation, tokenValidation } from '../../utils'
import { tokenValidation, updatePasswordValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()
const controller = new AuthController()
authRouter.patch(
'/updatePassword',
authenticateAccessToken,
async (req, res) => {
const { error, value: body } = updatePasswordValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
await controller.updatePassword(req, body)
res.sendStatus(204)
} catch (err: any) {
res.status(err.code).send(err.message)
}
}
)
authRouter.post('/token', async (req, res) => {
const { error, value: body } = tokenValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
+25
View File
@@ -0,0 +1,25 @@
import express from 'express'
import { AuthConfigController } from '../../controllers'
const authConfigRouter = express.Router()
authConfigRouter.get('/', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = controller.getDetail()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
authConfigRouter.post('/synchroniseWithLDAP', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = await controller.synchroniseWithLDAP()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
export default authConfigRouter
+16
View File
@@ -1,6 +1,7 @@
import express from 'express'
import { ClientController } from '../../controllers'
import { registerClientValidation } from '../../utils'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
const clientRouter = express.Router()
@@ -17,4 +18,19 @@ clientRouter.post('/', async (req, res) => {
}
})
clientRouter.get(
'/',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const controller = new ClientController()
try {
const response = await controller.getAllClients()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
export default clientRouter
+19 -1
View File
@@ -1,5 +1,5 @@
import express from 'express'
import { runCodeValidation } from '../../utils'
import { runCodeValidation, triggerCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/'
const runRouter = express.Router()
@@ -28,4 +28,22 @@ runRouter.post('/execute', async (req, res) => {
}
})
runRouter.post('/trigger', async (req, res) => {
const { error, value: body } = triggerCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerCode(req, body)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter
+7 -35
View File
@@ -18,11 +18,7 @@ groupRouter.post(
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -33,11 +29,7 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -49,11 +41,7 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -71,11 +59,7 @@ groupRouter.get(
const response = await controller.getGroupByGroupName(name)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -95,11 +79,7 @@ groupRouter.post(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -119,11 +99,7 @@ groupRouter.delete(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -140,11 +116,7 @@ groupRouter.delete(
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
+9
View File
@@ -18,6 +18,7 @@ import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
import permissionRouter from './permission'
import authConfigRouter from './authConfig'
const router = express.Router()
@@ -43,6 +44,14 @@ router.use(
permissionRouter
)
router.use(
'/authConfig',
desktopRestrict,
authenticateAccessToken,
verifyAdmin,
authConfigRouter
)
router.use(
'/',
swaggerUi.serve,
+22 -1
View File
@@ -1,16 +1,37 @@
import express from 'express'
import { SessionController } from '../../controllers'
import { sessionIdValidation } from '../../utils'
const sessionRouter = express.Router()
const controller = new SessionController()
sessionRouter.get('/', async (req, res) => {
const controller = new SessionController()
try {
const response = await controller.session(req)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sessionRouter.get('/:sessionId/state', async (req, res) => {
const { error, value: params } = sessionIdValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.sessionState(params.sessionId)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default sessionRouter
+90 -16
View File
@@ -5,6 +5,7 @@ import request from 'supertest'
import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../../../model/Client'
const client = {
clientId: 'someclientID',
@@ -26,6 +27,7 @@ describe('client', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
const userController = new UserController()
const clientController = new ClientController()
@@ -34,6 +36,18 @@ describe('client', () => {
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterAll(async () => {
@@ -43,22 +57,6 @@ describe('client', () => {
})
describe('create', () => {
let adminAccessToken: string
beforeAll(async () => {
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
@@ -157,4 +155,80 @@ describe('client', () => {
expect(res.body).toEqual({})
})
})
describe('get', () => {
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
await collection.deleteMany({})
})
it('should respond with an array of all clients', async () => {
await clientController.createClient(newClient)
await clientController.createClient({
clientId: 'clientID',
clientSecret: 'clientSecret'
})
const res = await request(app)
.get('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const expected = [
{
clientId: 'newClientID',
clientSecret: 'newClientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'clientID',
clientSecret: 'clientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
]
expect(res.body).toEqual(expected)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/client').send().expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbideen if access token is not of an admin account', async () => {
const user = {
displayName: 'User 2',
username: 'username2',
password: '12345678',
isAdmin: false,
isActive: true
}
const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
accessToken,
'refreshToken'
)
const res = await request(app)
.get('/SASjsApi/client')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
})
})
+132
View File
@@ -0,0 +1,132 @@
import path from 'path'
import { createFile, fileExists } from '@sasjs/utils'
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import {
UserController,
PermissionController,
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
RunTimeType,
sysInitCompiledPath
} from '../../../utils'
// Real, unmocked end-to-end test of the SAS execution pipeline (session
// spawn -> autoexec handshake -> exit code -> HTTP response), using a fake
// "SAS executable" (mockSas.js) in place of a real SAS install. This is the
// regression test for issue #388: SAS code that aborts (%abort;) used to
// hang the request forever instead of returning an error response.
const mockSasPath = path.join(__dirname, 'files', 'mockSas.js')
const clientId = 'codeSpecClientID'
const user = {
displayName: 'Code Spec User',
username: 'codeSpecUsername',
password: '87654321',
isAdmin: false,
isActive: true
}
let app: Express
let accessToken: string
describe('code', () => {
let con: Mongoose
let mongoServer: MongoMemoryServer
let userController: UserController
let permissionController: PermissionController
beforeAll(async () => {
// SASSessionController.createSession() unconditionally reads this file
// (Session.ts:105) before it ever spawns the SAS process. It's normally
// produced by `npm run compileSysInit` (part of the `initial`/`build`
// scripts), which `npm test` does not run - so on a fresh checkout
// (e.g. CI, where "Run Unit Tests" happens before "Build Package") it
// doesn't exist yet. Content is irrelevant here since mockSas.js never
// interprets it; it just needs to exist and be readable.
if (!(await fileExists(sysInitCompiledPath))) {
await createFile(sysInitCompiledPath, '')
}
mongoServer = await MongoMemoryServer.create()
process.env.DB_CONNECT = mongoServer.getUri()
process.env.MODE = 'server'
process.env.RUN_TIMES = 'sas'
process.env.SAS_PATH = mockSasPath
const appPromise = (await import('../../../app')).default
app = await appPromise
con = await mongoose.connect(mongoServer.getUri())
userController = new UserController()
permissionController = new PermissionController()
const dbUser = await userController.createUser(user)
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
path: '/SASjsApi/code/execute',
type: PermissionType.route,
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSettingForRoute.grant
})
process.runTimes = [RunTimeType.SAS]
process.sasLoc = mockSasPath
}, 30000)
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('execute', () => {
it('returns 200 with the mock log when the SAS program completes normally', async () => {
const response = await request(app)
.post('/SASjsApi/code/execute')
.auth(accessToken, { type: 'bearer' })
.send({ code: '%put hello world;', runTime: 'sas' })
.expect(200)
expect(response.text).toEqual(
expect.stringContaining('mock SAS execution')
)
}, 30000)
it('returns a prompt 400 (not a hang) with the complete log when the SAS session fails (%abort;)', async () => {
const response = await request(app)
.post('/SASjsApi/code/execute')
.auth(accessToken, { type: 'bearer' })
.send({ code: '%abort;', runTime: 'sas' })
.expect(400)
expect(response.body).toMatchObject({
status: 'failure',
message: 'Job execution failed.'
})
expect(response.body.log).toEqual(
expect.stringContaining('mock SAS execution')
)
}, 30000)
})
})
const generateAndSaveToken = async (userId: number) => {
const accessToken = generateAccessToken({
clientId,
userId
})
await saveTokensInDB(userId, clientId, accessToken, 'refreshToken')
return accessToken
}
+9
View File
@@ -28,6 +28,15 @@ jest
.spyOn(fileUtilModules, 'getUploadsFolder')
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
// getFilesFolder() resolves via getSasjsDriveFolder()/process.driveLoc, a
// separate root from getSasjsRootFolder()/process.sasjsRoot above - without
// this, every test in this file that creates content under the drive (e.g.
// 'level1', 'my/path/...') writes into the real, shared sasjs_root/drive
// instead of this run's isolated tmpFolder, leaking state into later runs.
jest
.spyOn(fileUtilModules, 'getFilesFolder')
.mockImplementation(() => path.join(tmpFolder, 'drive', 'files'))
import appPromise from '../../../app'
import {
UserController,
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env node
// Minimal fake "SAS executable" used by tests in place of a real SAS
// install. It only fulfils the CLI/filesystem handshake contract that
// Session.ts / processProgram.ts rely on:
//
// -SYSIN <path> the file used as a signal channel: it starts out
// containing a dummy (empty) placeholder, we delete it
// to signal "session ready", then wait for it to be
// rewritten with the real submitted program.
// -LOG <path> where we write a fake log so downstream fileExists()/
// readFile() calls have something to find.
//
// It does NOT interpret real SAS syntax. Exit code is the only thing that
// matters to the Node side: 0 mimics a normal SAS termination, non-zero
// mimics an abnormal one (e.g. %abort;).
const fs = require('fs')
const arg = (flag) => {
const idx = process.argv.indexOf(flag)
return idx === -1 ? undefined : process.argv[idx + 1]
}
const sysin = arg('-SYSIN')
const logPath = arg('-LOG')
// Give up waiting for the real program after this long, so an unused
// pre-warmed session (sasjs pools up to 3 ready sessions) doesn't linger
// forever and keep a test process alive. Generous rather than tight: CI
// runners are frequently slower/more contended than a local dev machine
// (shared CPU, coverage instrumentation on the Node side slowing down the
// round trip this process is waiting on), and this cost is only ever paid
// by sessions nothing is actually waiting on - the session actually used
// by a request gets its real code written far sooner than this in practice.
const GIVE_UP_AFTER_MS = 8000
const sleepSync = (ms) => {
const until = Date.now() + ms
while (Date.now() < until) {
/* busy-wait, mirroring the real autoexec's SAS-side sleep() loop */
}
}
// Any of the filesystem calls below can legitimately race against
// processProgram's own write-then-rename (e.g. existsSync sees the file,
// then a rename mid-flight makes the following read miss). Treat that as
// "not ready yet" and retry a few times, rather than letting an uncaught
// exception crash this process with a non-zero exit - which would be
// indistinguishable, to the Node side, from a genuine SAS failure.
const retry = (fn, attempts = 5, delayMs = 20) => {
for (let i = 0; i < attempts; i++) {
try {
return { ok: true, value: fn() }
} catch (err) {
if (i === attempts - 1) return { ok: false, error: err }
sleepSync(delayMs)
}
}
}
// 1. remove the dummy SYSIN, signalling "session ready" to waitForSession()
if (sysin) {
retry(() => {
if (fs.existsSync(sysin)) fs.unlinkSync(sysin)
})
}
// 2. wait for the real program to be written back to the same path
const deadline = Date.now() + GIVE_UP_AFTER_MS
while (!sysin || !fs.existsSync(sysin)) {
if (Date.now() > deadline) process.exit(0)
sleepSync(10)
}
// small settle delay, mirroring the real autoexec's sleep(0.01,1) after
// detecting the file, so a fast-moving rename isn't read mid-write
sleepSync(50)
const readResult = retry(() => fs.readFileSync(sysin, 'utf-8'))
if (!readResult.ok) {
process.stderr.write(
`mockSas.js: failed to read ${sysin}: ${readResult.error}\n`
)
process.exit(1)
}
const code = readResult.value
if (logPath) {
retry(() => fs.writeFileSync(logPath, `NOTE: mock SAS execution\n${code}\n`))
}
if (code.includes('%abort;')) {
process.stderr.write('ERROR: SAS session terminated. See log for details.\n')
process.exit(1)
}
process.exit(0)
+87 -2
View File
@@ -4,8 +4,13 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { PUBLIC_GROUP_NAME } from '../../../model/Group'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
import User from '../../../model/User'
const clientId = 'someclientID'
const adminUser = {
@@ -560,6 +565,46 @@ describe('group', () => {
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
)
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'ldapGroupUser'
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'ldapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
})
describe('RemoveUser', () => {
@@ -611,6 +656,46 @@ describe('group', () => {
expect(res.body.groups).toEqual([])
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'removeLdapGroupUser'
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'removeLdapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
+5 -13
View File
@@ -21,12 +21,11 @@ import {
} from '../../../utils'
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController,
PythonSessionController
SessionController,
SASSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types'
import { Session, SessionState } from '../../../types'
const clientId = 'someclientID'
@@ -472,11 +471,7 @@ const setupMocks = async () => {
.mockImplementation(mockedGetSession)
jest
.spyOn(JSSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(PythonSessionController.prototype, 'getSession')
.spyOn(SASSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
@@ -498,10 +493,7 @@ const mockedGetSession = async () => {
const session: Session = {
id: sessionId,
ready: true,
inUse: true,
consumed: false,
completed: false,
state: SessionState.pending,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
+57 -22
View File
@@ -4,7 +4,12 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import User from '../../../model/User'
const clientId = 'someclientID'
const adminUser = {
@@ -110,16 +115,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
await controller.createUser(user)
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send(user)
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
@@ -226,6 +231,36 @@ describe('user', () => {
.expect(400)
})
it('should respond with Method Not Allowed, when updating username of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newUsername = 'newUsername'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ username: newUsername })
.expect(405)
})
it('should respond with Method Not Allowed, when updating displayName of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newDisplayName = 'My new display Name'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ displayName: newDisplayName })
.expect(405)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/user/1234')
@@ -254,7 +289,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -265,9 +300,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
@@ -349,7 +384,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
it('should respond with Conflict if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -360,9 +395,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.text).toEqual('Username already exists.')
expect(res.body).toEqual({})
})
})
@@ -446,7 +481,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
@@ -454,9 +489,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
@@ -528,7 +563,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
@@ -536,9 +571,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
})
@@ -652,16 +687,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if userId is incorrect', async () => {
it('should respond with Not Found if userId is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User is not found.')
expect(res.text).toEqual('User is not found.')
expect(res.body).toEqual({})
})
@@ -731,16 +766,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is incorrect', async () => {
it('should respond with Not Found if username is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/by/username/randomUsername')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User is not found.')
expect(res.text).toEqual('User is not found.')
expect(res.body).toEqual({})
})
})
+165 -58
View File
@@ -47,12 +47,82 @@ describe('web', () => {
})
})
describe('SASLogon/login', () => {
describe('SASLogon/authorize', () => {
let csrfToken: string
let cookies: string
let authCookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
;({ csrfToken } = await getCSRF(app))
await userController.createUser(user)
const credentials = {
username: user.username,
password: user.password
}
;({ authCookies } = await performLogin(app, credentials, csrfToken))
})
afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({ clientId })
expect(res.body).toHaveProperty('code')
})
it('should respond with Bad Request if CSRF Token is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.send({ clientId })
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
})
.expect(403)
expect(res.text).toEqual('Error: Invalid clientId.')
expect(res.body).toEqual({})
})
})
describe('SASLogon/login', () => {
let csrfToken: string
beforeAll(async () => {
;({ csrfToken } = await getCSRF(app))
})
afterEach(async () => {
@@ -66,7 +136,6 @@ describe('web', () => {
const res = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
@@ -79,73 +148,113 @@ describe('web', () => {
id: expect.any(Number),
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin
isAdmin: user.isAdmin,
needsToUpdatePassword: true
})
})
})
describe('SASLogon/authorize', () => {
let csrfToken: string
let cookies: string
let authCookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
it('should respond with too many requests when attempting with invalid password for a same user too many times', async () => {
await userController.createUser(user)
const credentials = {
username: user.username,
password: user.password
}
const promises: request.Test[] = []
;({ cookies: authCookies } = await performLogin(
app,
credentials,
cookies,
csrfToken
))
})
const maxConsecutiveFailsByUsernameAndIp = Number(
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
)
afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
Array(maxConsecutiveFailsByUsernameAndIp + 1)
.fill(0)
.map((_, i) => {
promises.push(
request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: 'invalid-password'
})
)
})
await Promise.all(promises)
it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({ clientId })
.send({
username: user.username,
password: user.password
})
.expect(429)
expect(res.body).toHaveProperty('code')
expect(res.text).toContain('Too Many Requests!')
})
it('should respond with Bad Request if clientId is missing', async () => {
it('should respond with too many requests when attempting with invalid credentials for different users but with same ip too many times', async () => {
await userController.createUser(user)
const promises: request.Test[] = []
const maxWrongAttemptsByIpPerDay = Number(
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
)
Array(maxWrongAttemptsByIpPerDay + 1)
.fill(0)
.map((_, i) => {
promises.push(
request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: `user${i}`,
password: 'invalid-password'
})
)
})
await Promise.all(promises)
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({})
.send({
username: user.username,
password: user.password
})
.expect(429)
expect(res.text).toContain('Too Many Requests!')
})
it('should respond with Bad Request if CSRF Token is not present', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
})
.expect(403)
it('should respond with Bad Request if CSRF Token is invalid', async () => {
await userController.createUser(user)
expect(res.text).toEqual('Error: Invalid clientId.')
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
})
@@ -153,27 +262,25 @@ describe('web', () => {
const getCSRF = async (app: Express) => {
// make request to get CSRF
const { header, text } = await request(app).get('/')
const cookies = header['set-cookie'].join()
const { text } = await request(app).get('/')
const csrfToken = extractCSRF(text)
return { csrfToken, cookies }
return { csrfToken: extractCSRF(text) }
}
const performLogin = async (
app: Express,
credentials: { username: string; password: string },
cookies: string,
csrfToken: string
) => {
const { header } = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send(credentials)
const newCookies: string = header['set-cookie'].join()
return { cookies: newCookies }
return {
authCookies:
(header['set-cookie'] as unknown as string[] | undefined)?.join() || ''
}
}
const extractCSRF = (text: string) =>
+33 -2
View File
@@ -1,5 +1,8 @@
import express from 'express'
import { executeProgramRawValidation } from '../../utils'
import {
executeProgramRawValidation,
triggerProgramValidation
} from '../../utils'
import { STPController } from '../../controllers/'
import { FileUploadController } from '../../controllers/internal'
@@ -13,7 +16,11 @@ stpRouter.get('/execute', async (req, res) => {
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeGetRequest(req, query._program)
const response = await controller.executeGetRequest(
req,
query._program,
query._debug
)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
@@ -65,4 +72,28 @@ stpRouter.post(
}
)
stpRouter.post('/trigger', async (req, res) => {
const { error, value: query } = triggerProgramValidation(req.query)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerProgram(
req,
query._program,
query._debug,
query.expiresAfterMins
)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default stpRouter
+8 -8
View File
@@ -23,7 +23,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
const response = await controller.createUser(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -33,7 +33,7 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllUsers()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -51,7 +51,7 @@ userRouter.get(
const response = await controller.getUserByUsername(req, username)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -64,7 +64,7 @@ userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const response = await controller.getUser(req, parseInt(userId))
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
})
@@ -91,7 +91,7 @@ userRouter.patch(
const response = await controller.updateUserByUsername(username, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -113,7 +113,7 @@ userRouter.patch(
const response = await controller.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -141,7 +141,7 @@ userRouter.delete(
await controller.deleteUserByUsername(username, data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
@@ -163,7 +163,7 @@ userRouter.delete(
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)
+3 -3
View File
@@ -1,6 +1,6 @@
import path from 'path'
import express, { Request } from 'express'
import { authenticateAccessToken } from '../../middlewares'
import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
@@ -13,7 +13,7 @@ const router = express.Router()
router.get('/', authenticateAccessToken, async (req, res) => {
const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken())
res.cookie('XSRF-TOKEN', generateCSRFToken())
return res.send(content)
})
@@ -58,7 +58,7 @@ export const publishAppStream = async (
)
const sasJsPort = process.env.PORT || 5000
console.log(
process.logger.info(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)
+2 -2
View File
@@ -4,7 +4,7 @@ import webRouter from './web'
import apiRouter from './api'
import appStreamRouter from './appStream'
import { csrfProtection } from '../app'
import { csrfProtection } from '../middlewares'
export const setupRoutes = (app: Express) => {
app.use('/SASjsApi', apiRouter)
@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
appStreamRouter(req, res, next)
})
app.use('/', csrfProtection, webRouter)
app.use('/', webRouter)
}
+19 -1
View File
@@ -1,8 +1,26 @@
import express from 'express'
import sas9WebRouter from './sas9-web'
import sasViyaWebRouter from './sasviya-web'
import webRouter from './web'
import { MOCK_SERVERTYPEType } from '../../utils'
import { csrfProtection } from '../../middlewares'
const router = express.Router()
router.use('/', webRouter)
const { MOCK_SERVERTYPE } = process.env
switch (MOCK_SERVERTYPE) {
case MOCK_SERVERTYPEType.SAS9: {
router.use('/', sas9WebRouter)
break
}
case MOCK_SERVERTYPEType.SASVIYA: {
router.use('/', sasViyaWebRouter)
break
}
default: {
router.use('/', csrfProtection, webRouter)
}
}
export default router
+152
View File
@@ -0,0 +1,152 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers'
import { MockSas9Controller } from '../../controllers/mock-sas9'
import multer from 'multer'
import path from 'path'
import dotenv from 'dotenv'
import { FileUploadController } from '../../controllers/internal'
dotenv.config()
const sas9WebRouter = express.Router()
const webController = new WebController()
// Mock controller must be singleton because it keeps the states
// for example `isLoggedIn` and potentially more in future mocks
const controller = new MockSas9Controller()
const fileUploadController = new FileUploadController()
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
const upload = multer({
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
})
sas9WebRouter.get('/', async (req, res) => {
let response
try {
response = await webController.home()
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(injectedContent)
}
})
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
const response = await controller.sasStoredProcess(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
const response = await controller.sasStoredProcessDoGet(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.post(
'/SASStoredProcess/do/',
fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(),
async (req, res) => {
const response = await controller.sasStoredProcessDoPost(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
const response = await controller.loginGet()
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
const response = await controller.loginPost(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASLogon/logout', async (req, res) => {
const response = await controller.logout(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASStoredProcess/Logoff', async (req, res) => {
const response = await controller.logoff(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sas9WebRouter
+33
View File
@@ -0,0 +1,33 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
const sasViyaWebRouter = express.Router()
const controller = new WebController()
sasViyaWebRouter.get('/', async (req, res) => {
let response
try {
response = await controller.home()
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(injectedContent)
}
})
sasViyaWebRouter.post('/SASJobExecution/', async (req, res) => {
try {
res.send({ test: 'test' })
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sasViyaWebRouter
+28 -11
View File
@@ -1,6 +1,11 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import {
authenticateAccessToken,
bruteForceProtection,
desktopRestrict
} from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils'
const webRouter = express.Router()
@@ -13,7 +18,10 @@ webRouter.get('/', async (req, res) => {
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const { ALLOWED_DOMAIN } = process.env
const allowedDomain = ALLOWED_DOMAIN?.trim()
const domain = allowedDomain ? ` Domain=${allowedDomain};` : ''
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()};${domain} Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
@@ -23,17 +31,26 @@ webRouter.get('/', async (req, res) => {
}
})
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
webRouter.post(
'/SASLogon/login',
desktopRestrict,
bruteForceProtection,
async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
if (err instanceof Error) {
res.status(500).send(err.toString())
} else {
res.status(err.code).send(err.message)
}
}
}
})
)
webRouter.post(
'/SASLogon/authorize',
+3 -3
View File
@@ -7,11 +7,11 @@ appPromise.then(async (app) => {
const protocol = process.env.PROTOCOL || 'http'
const sasJsPort = process.env.PORT || 5000
console.log('PROTOCOL: ', protocol)
process.logger.info('PROTOCOL: ', protocol)
if (protocol !== 'https') {
app.listen(sasJsPort, () => {
console.log(
process.logger.info(
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
)
})
@@ -20,7 +20,7 @@ appPromise.then(async (app) => {
const httpsServer = createServer({ key, cert, ca }, app)
httpsServer.listen(sasJsPort, () => {
console.log(
process.logger.info(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
)
})
+1
View File
@@ -5,5 +5,6 @@ export interface RequestUser {
displayName: string
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec?: string
}
+10 -5
View File
@@ -1,11 +1,16 @@
export enum SessionState {
initialising = 'initialising', // session is initialising and not ready to be used yet
pending = 'pending', // session is ready to be used
running = 'running', // session is in use
completed = 'completed', // session is completed and can be destroyed
failed = 'failed' // session failed
}
export interface Session {
id: string
ready: boolean
state: SessionState
creationTimeStamp: string
deathTimeStamp: string
path: string
inUse: boolean
consumed: boolean
completed: boolean
crashed?: string
expiresAfterMins?: { mins: number; used: boolean }
failureReason?: string
}
+3 -2
View File
@@ -3,12 +3,13 @@ declare namespace NodeJS {
sasLoc?: string
nodeLoc?: string
pythonLoc?: string
rLoc?: string
driveLoc: string
sasjsRoot: string
logsLoc: string
logsUUID: string
sessionController?: import('../../controllers/internal').SessionController
sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
pythonSessionController?: import('../../controllers/internal').PythonSessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]
+1 -1
View File
@@ -36,7 +36,7 @@ export const loadAppStreamConfig = async () => {
)
}
console.log('App Stream Config loaded!')
process.logger.info('App Stream Config loaded!')
}
export const addEntryToAppStreamConfig = (
+1 -1
View File
@@ -8,6 +8,6 @@ export const connectDB = async () => {
throw new Error('Unable to connect to DB!')
}
console.log('Connected to DB!')
process.logger.success('Connected to DB!')
return seedDB()
}
+2 -2
View File
@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
if (process.env.NODE_ENV === 'test') return
console.log('Copying Macros from container to drive(tmp).')
process.logger.log('Copying Macros from container to drive.')
const macrosDrivePath = getMacrosFolder()
@@ -30,5 +30,5 @@ export const copySASjsCore = async () => {
await createFile(macroFileDestPath, macroContent)
})
console.log('Macros Drive Path:', macrosDrivePath)
process.logger.info('Macros Drive Path:', macrosDrivePath)
}
+18
View File
@@ -0,0 +1,18 @@
import path from 'path'
import { createFile } from '@sasjs/utils'
import { getMacrosFolder } from './file'
const fileContent = `%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO,maxobs=MAX);
%ms_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt
,missing=&missing
,showmeta=&showmeta
,maxobs=&maxobs
)
%mend;`
export const createWeboutSasFile = async () => {
const macrosDrivePath = getMacrosFolder()
process.logger.log(`Creating webout.sas at ${macrosDrivePath}`)
const filePath = path.join(macrosDrivePath, 'webout.sas')
await createFile(filePath, fileContent)
}
+10 -5
View File
@@ -10,7 +10,7 @@ export const sysInitCompiledPath = path.join(
'systemInitCompiled.sas'
)
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const sasJSCoreMacros = path.join(apiRoot, 'sas', 'sasautos')
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
@@ -20,19 +20,24 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
export const getDesktopUserAutoExecPath = () =>
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
export const getSasjsRootFolder = () => process.driveLoc
export const getSasjsRootFolder = () => process.sasjsRoot
export const getSasjsDriveFolder = () => process.driveLoc
export const getLogFolder = () => process.logsLoc
export const getAppStreamConfigPath = () =>
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
export const getMacrosFolder = () =>
path.join(getSasjsRootFolder(), 'sasjscore')
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
export const getPackagesFolder = () =>
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
+3 -2
View File
@@ -1,7 +1,8 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateAccessToken = (data: InfoJWT) =>
export const generateAccessToken = (data: InfoJWT, expiry?: number) =>
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
expiresIn: '1day'
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
})
+3 -2
View File
@@ -1,7 +1,8 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateRefreshToken = (data: InfoJWT) =>
export const generateRefreshToken = (data: InfoJWT, expiry?: number) =>
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
expiresIn: '30 days'
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
})
+3 -2
View File
@@ -1,7 +1,8 @@
import { Request } from 'express'
export const TopLevelRoutes = ['/AppStream', '/SASjsApi']
const StaticAuthorizedRoutes = [
'/AppStream',
'/SASjsApi/code/execute',
'/SASjsApi/stp/execute',
'/SASjsApi/drive/deploy',
@@ -15,7 +16,7 @@ const StaticAuthorizedRoutes = [
export const getAuthorizedRoutes = () => {
const streamingApps = Object.keys(process.appStreamConfig)
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
return [...TopLevelRoutes, ...StaticAuthorizedRoutes, ...streamingAppsRoutes]
}
export const getPath = (req: Request) => {
+3 -3
View File
@@ -10,9 +10,9 @@ export const getCertificates = async () => {
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
const caPath = CA_ROOT
console.log('keyPath: ', keyPath)
console.log('certPath: ', certPath)
if (caPath) console.log('caPath: ', caPath)
process.logger.info('keyPath: ', keyPath)
process.logger.info('certPath: ', certPath)
if (caPath) process.logger.info('caPath: ', caPath)
const key = await readFile(keyPath)
const cert = await readFile(certPath)
+29 -3
View File
@@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
import { RunTimeType } from './verifyEnvVariables'
export const getDesktopFields = async () => {
const { SAS_PATH, NODE_PATH, PYTHON_PATH } = process.env
const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env
let sasLoc, nodeLoc, pythonLoc
let sasLoc, nodeLoc, pythonLoc, rLoc
if (process.runTimes.includes(RunTimeType.SAS)) {
sasLoc = SAS_PATH ?? (await getSASLocation())
@@ -20,7 +20,11 @@ export const getDesktopFields = async () => {
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
}
return { sasLoc, nodeLoc, pythonLoc }
if (process.runTimes.includes(RunTimeType.R)) {
rLoc = R_PATH ?? (await getRLocation())
}
return { sasLoc, nodeLoc, pythonLoc, rLoc }
}
const getDriveLocation = async (): Promise<string> => {
@@ -117,3 +121,25 @@ const getPythonLocation = async (): Promise<string> => {
return targetName
}
const getRLocation = async (): Promise<string> => {
const validator = async (filePath: string) => {
if (!filePath) return 'Path to R executable is required.'
if (!(await fileExists(filePath))) {
return 'No file found at provided path.'
}
return true
}
const defaultLocation = isWindows() ? 'C:\\Rscript' : '/usr/bin/Rscript'
const targetName = await getString(
'Please enter full path to a R executable: ',
validator,
defaultLocation
)
return targetName
}
+5 -5
View File
@@ -7,7 +7,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const { user, accessToken } = req
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
const sessionId = req.cookies['connect.sid']
const { _csrf } = req.cookies
const httpHeaders: string[] = []
@@ -16,14 +15,15 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const cookies: string[] = []
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
if (_csrf) cookies.push(`_csrf=${_csrf}`)
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
//In desktop mode when mocking mode is enabled, user was undefined.
//So this is workaround.
return {
username: user!.username,
userId: user!.userId,
displayName: user!.displayName,
username: user ? user.username : 'demo',
userId: user ? user.userId : 0,
displayName: user ? user.displayName : 'demo',
serverUrl: protocol + host,
httpHeaders
}
+2 -2
View File
@@ -4,8 +4,8 @@ import { getFilesFolder } from './file'
import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath)
// If programPath (_program) is provided with a ".sas", ".js" or ".py" extension
const ext = path.extname(programPath).toLowerCase()
// If programPath (_program) is provided with a ".sas", ".js", ".py" or ".r" extension
// we should use that extension to determine the appropriate runTime
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
const runTime = ext.slice(1)
+15
View File
@@ -0,0 +1,15 @@
import Counter from '../model/Counter'
export const getSequenceNextValue = async (seqName: string) => {
const seqDoc = await Counter.findOne({ id: seqName })
if (!seqDoc) {
await Counter.create({ id: seqName, seq: 1 })
return 1
}
seqDoc.seq += 1
await seqDoc.save()
return seqDoc.seq
}
+57
View File
@@ -0,0 +1,57 @@
import jwt from 'jsonwebtoken'
import User from '../model/User'
import { InfoJWT } from '../types/InfoJWT'
const isValidToken = async (
token: string,
key: string,
userId: number,
clientId: string
) => {
const promise = new Promise<boolean>((resolve, reject) =>
jwt.verify(token, key, (err, decoded) => {
if (err) return reject(false)
const payload = decoded as InfoJWT
if (payload?.userId === userId && payload?.clientId === clientId) {
return resolve(true)
}
return reject(false)
})
)
return await promise.then(() => true).catch(() => false)
}
export const getTokensFromDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ id: userId })
if (!user) return
const currentTokenObj = user.tokens.find(
(tokenObj: any) => tokenObj.clientId === clientId
)
if (currentTokenObj) {
const accessToken = currentTokenObj.accessToken
const refreshToken = currentTokenObj.refreshToken
const isValidAccessToken = await isValidToken(
accessToken,
process.secrets.ACCESS_TOKEN_SECRET,
userId,
clientId
)
const isValidRefreshToken = await isValidToken(
refreshToken,
process.secrets.REFRESH_TOKEN_SECRET,
userId,
clientId
)
if (isValidAccessToken && isValidRefreshToken) {
return { accessToken, refreshToken }
}
}
}
+7 -1
View File
@@ -1,6 +1,7 @@
export * from './appStreamConfig'
export * from './connectDB'
export * from './copySASjsCore'
export * from './createWeboutSasFile'
export * from './desktopAutoExec'
export * from './extractHeaders'
export * from './extractName'
@@ -13,18 +14,23 @@ export * from './getCertificates'
export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './getRunTimeAndFilePath'
export * from './getSequenceNextValue'
export * from './getServerUrl'
export * from './getTokensFromDB'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './zipped'
export * from './ldapClient'
export * from './parseLogToArray'
export * from './rateLimiter'
export * from './removeTokensInDB'
export * from './saveTokensInDB'
export * from './seedDB'
export * from './setProcessVariables'
export * from './setupFolders'
export * from './setupUserAutoExec'
export * from './upload'
export * from './validation'
export * from './verifyEnvVariables'
export * from './verifyTokenInDB'
export * from './zipped'

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