mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
63 Commits
v0.34.2
...
cb2faee5a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb2faee5a7 | ||
|
|
c261745f1d | ||
|
|
d6e527ecf2 | ||
|
|
bc2cff1d0d | ||
|
|
66aa9b5891 | ||
|
|
ca17e7c192 | ||
|
|
73df102422 | ||
|
|
48a9a4dd0e | ||
|
|
4f6f735f5b | ||
|
|
6b6546c7ad | ||
|
|
f94ddc0352 | ||
|
|
03670cf0d6 | ||
|
|
ea2ec97c1c | ||
|
|
832f1156e8 | ||
|
|
5cda9cd5d8 | ||
|
|
5d576aff91 | ||
|
|
a044176054 | ||
|
|
deee34f5fd | ||
|
|
b0723f1444 | ||
|
|
e9519cb3c6 | ||
|
|
0838b8112e | ||
|
|
441f8b7726 | ||
|
|
049a7f4b80 | ||
|
|
3053c68bdf | ||
|
|
76750e864d | ||
|
|
ffcf193b87 | ||
|
|
aa2a1cbe13 | ||
|
|
6f2c53555c | ||
|
|
73d965daf5 | ||
|
|
4f1763db67 | ||
|
|
28222add04 | ||
|
|
068edfd6a5 | ||
|
|
7e8cbbf377 | ||
|
|
1fc1431442 | ||
|
|
3387efbb9a | ||
|
|
e2996b495f | ||
|
|
41c627f93a | ||
|
|
49f5dc7555 | ||
|
|
f6e77f99a4 | ||
|
|
b57dfa429b | ||
| 9586dbb2d0 | |||
|
|
a4f78ab48d | ||
|
|
2f47a2213b | ||
|
|
0f91395fbb | ||
|
|
167b14fed0 | ||
|
|
8940f4dc47 | ||
|
|
48c1ada1b6 | ||
|
|
0532488b55 | ||
|
|
d458b5bb81 | ||
|
|
958ab9cad2 | ||
|
|
78ceed13e1 | ||
|
|
a17814fc90 | ||
|
|
9aaffce820 | ||
|
|
e78f87f5c0 | ||
|
|
bd1b58086d | ||
|
|
9f521634d9 | ||
|
|
a696168443 | ||
|
|
31df72ad88 | ||
|
|
eb42683fff | ||
|
|
d2de9dc13e | ||
|
|
6dd2f4f876 | ||
|
|
c0f38ba7c9 | ||
|
|
b03a5db22f |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -56,4 +56,4 @@ jobs:
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"autoexec"
|
||||
]
|
||||
"cSpell.words": ["autoexec", "initialising"]
|
||||
}
|
||||
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -1,3 +1,80 @@
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ CORS=
|
||||
WHITELIST=
|
||||
|
||||
# HELMET Cross Origin Embedder Policy
|
||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||
# options: [true|false] default: true
|
||||
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
|
||||
HELMET_COEP=
|
||||
|
||||
@@ -1,84 +1,206 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="HJy_QFGaI9JSrArARLup" name="Page-1">
|
||||
<mxGraphModel dx="1908" dy="2140" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<mxGraphModel dx="3103" dy="2723" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="4" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
|
||||
<mxGeometry x="-360" y="-120" width="40" height="80" as="geometry"/>
|
||||
<mxCell id="7" value="SASjs Server" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;fontSize=30;" parent="1" vertex="1">
|
||||
<mxGeometry x="30" y="-150" width="360" height="1250" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="SASjs Server" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;fontSize=30;" vertex="1" parent="1">
|
||||
<mxGeometry x="30" y="-150" width="360" height="850" as="geometry"/>
|
||||
<mxCell id="36" value="<font style="font-size: 22px">Internal Authentication</font>" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="50" y="-60" width="320" height="330" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="28">
|
||||
<mxCell id="4" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" parent="1" vertex="1">
|
||||
<mxGeometry x="-740" y="-120" width="40" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" parent="1" target="28" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-340" y="23" as="sourcePoint"/>
|
||||
<mxPoint x="-530" y="23" as="sourcePoint"/>
|
||||
<mxPoint x="115" y="22.586363636363558" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/authorize<br>(username,password,clientId)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
|
||||
<mxCell id="11" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/authorize<br>(username,password,clientId)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="8" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="" style="edgeStyle=none;html=1;exitX=-0.002;exitY=0.874;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="28">
|
||||
<mxCell id="14" value="" style="edgeStyle=none;html=1;exitX=-0.002;exitY=0.874;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="28" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="110" y="80" as="sourcePoint"/>
|
||||
<mxPoint x="-340" y="80" as="targetPoint"/>
|
||||
<mxPoint x="-530" y="80" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`code`</span></font>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="14">
|
||||
<mxCell id="16" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`code`</span></font>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="14" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
|
||||
<mxGeometry x="-360" y="545" width="40" height="80" as="geometry"/>
|
||||
<mxCell id="21" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" parent="1" vertex="1">
|
||||
<mxGeometry x="-730" y="1100" width="40" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="30">
|
||||
<mxCell id="22" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" parent="1" target="30" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-340" y="165" as="sourcePoint"/>
|
||||
<mxPoint x="-530" y="163" as="sourcePoint"/>
|
||||
<mxPoint x="115" y="165" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/token</span></div><span style="color: #a31515">(clientId,code)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="22">
|
||||
<mxCell id="23" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/token</span></div><span style="color: #a31515">(clientId,code)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="22" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="" style="edgeStyle=none;html=1;exitX=0.009;exitY=0.905;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="30">
|
||||
<mxCell id="24" value="" style="edgeStyle=none;html=1;exitX=0.009;exitY=0.905;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="30" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="210" y="222.5" as="sourcePoint"/>
|
||||
<mxPoint x="-340" y="223" as="targetPoint"/>
|
||||
<mxPoint x="-530" y="223" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`</span></font><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">accessToken</span><span style="font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace">` &amp; `</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">refreshToken</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">`</span>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="24">
|
||||
<mxCell id="25" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`</span></font><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">accessToken</span><span style="font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace">` &amp; `</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">refreshToken</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">`</span>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="24" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;" edge="1" parent="1" source="21" target="4">
|
||||
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;" parent="1" source="21" target="4" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="40" y="240" as="sourcePoint"/>
|
||||
<mxPoint x="90" y="190" as="targetPoint"/>
|
||||
<mxPoint x="-340" y="240" as="sourcePoint"/>
|
||||
<mxPoint x="-290" y="190" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="<span>Validates</span><br><span>username/password/clientId</span><br><span>and issue short</span><br><span>Authorization code</span>" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxCell id="28" value="<span>Validates</span><br><span>username/password/clientId</span><br><span>and issue short</span><br><span>Authorization code</span>" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="115" width="190" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="Validates<br>clientId &amp; authorization code<br>and issue<br>Access Token &amp; Refresh Token" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxCell id="30" value="Validates<br>clientId &amp; authorization code<br>and issue<br>Access Token &amp; Refresh Token" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="115" y="140" width="190" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="Protected APIs<br>Authenticate requests <br>with provided Bearer Token" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="50" y="280" width="320" height="400" as="geometry"/>
|
||||
<mxCell id="32" value="Protected APIs<br>Authenticate requests <br>with provided Bearer Token" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="50" y="920" width="320" height="150" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="33" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.373;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="32">
|
||||
<mxCell id="33" value="" style="edgeStyle=none;html=1;entryX=-0.012;entryY=0.384;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="32" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-340" y="432.5" as="sourcePoint"/>
|
||||
<mxPoint x="-10" y="430" as="targetPoint"/>
|
||||
<mxPoint x="-520" y="978" as="sourcePoint"/>
|
||||
<mxPoint x="-80" y="819" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><font color="#a31515">Request with Access Token</font></div></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33">
|
||||
<mxCell id="34" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><font color="#a31515">Request with Access Token</font></div></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="33" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="37" value="Browser" style="rounded=0;whiteSpace=wrap;html=1;fontSize=22;" vertex="1" parent="1">
|
||||
<mxGeometry x="-590" y="-100" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="Browser" style="rounded=0;whiteSpace=wrap;html=1;fontSize=22;" vertex="1" parent="1">
|
||||
<mxGeometry x="-590" y="1110" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="39" value="" style="endArrow=none;dashed=1;html=1;fontSize=22;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="38" target="37">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-340" y="390" as="sourcePoint"/>
|
||||
<mxPoint x="-290" y="340" as="targetPoint"/>
|
||||
<Array as="points"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="40" value="<font style="font-size: 22px">Okta Authentication</font>" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="50" y="300" width="320" height="560" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="41" value="" style="edgeStyle=none;html=1;entryX=-0.013;entryY=0.092;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="49">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-530" y="373" as="sourcePoint"/>
|
||||
<mxPoint x="115" y="372.58636363636356" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="42" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/okta/authorize<br></span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="41">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="49" value="redirects to okta server<br><br><br><br>OKTA OIDC middleware<br>https://github.com/okta/okta-oidc-middleware<br><br><br><br>OKTA nodeJS Express implementation<br>https://github.com/okta/samples-nodejs-express-4/tree/master/okta-hosted-login<br>" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="115" y="350" width="190" height="280" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="75" style="edgeStyle=none;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;" edge="1" parent="1" source="50" target="32">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="50" value="Validates express session <br>through OKTA OIDC middleware" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="115" y="710" width="190" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="51" value="<span style="font-size: 22px">Okta Authorization Server<br></span>" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="600" y="300" width="320" height="380" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="52" value="" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="665" y="350" width="190" height="280" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="53" value="" style="endArrow=classic;html=1;fontSize=22;exitX=1.002;exitY=0.123;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="49">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="400" y="480" as="sourcePoint"/>
|
||||
<mxPoint x="660" y="384" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="54" value="<span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">/authorize</span>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=22;" vertex="1" connectable="0" parent="53">
|
||||
<mxGeometry x="0.0222" y="1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="58" value="" style="endArrow=classic;html=1;fontSize=22;exitX=-0.016;exitY=0.291;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="52">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-300" y="470" as="sourcePoint"/>
|
||||
<mxPoint x="-530" y="431" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="61" value="302 redirect to authentication prompt&nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=22;" vertex="1" connectable="0" parent="58">
|
||||
<mxGeometry x="-0.659" y="3" relative="1" as="geometry">
|
||||
<mxPoint x="-630" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="62" value="" style="endArrow=classic;html=1;fontSize=22;" edge="1" parent="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-530" y="500" as="sourcePoint"/>
|
||||
<mxPoint x="665" y="500.48" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="64" value="Authentication &amp; Consent" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=22;" vertex="1" connectable="0" parent="62">
|
||||
<mxGeometry x="-0.4695" y="6" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="65" value="" style="endArrow=classic;html=1;fontSize=22;exitX=0;exitY=0.679;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.995;entryY=0.679;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="52" target="49">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="230" y="560" as="sourcePoint"/>
|
||||
<mxPoint x="280" y="510" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="66" value="Authorization Code to redirect uri" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" vertex="1" connectable="0" parent="65">
|
||||
<mxGeometry x="0.0583" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="26" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="67" value="" style="endArrow=classic;html=1;fontSize=22;exitX=0;exitY=0.679;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.995;entryY=0.679;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="304.0500000000002" y="569.9999999999999" as="sourcePoint"/>
|
||||
<mxPoint x="665.0000000000005" y="569.9999999999999" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="68" value="<span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">/token</span>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" vertex="1" connectable="0" parent="67">
|
||||
<mxGeometry x="0.0583" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="-5" y="1" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="69" value="" style="endArrow=classic;html=1;fontSize=22;exitX=0;exitY=0.679;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.995;entryY=0.679;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="665.9500000000005" y="599.9999999999999" as="sourcePoint"/>
|
||||
<mxPoint x="305.00000000000017" y="599.9999999999999" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="70" value="Access Token" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" vertex="1" connectable="0" parent="69">
|
||||
<mxGeometry x="0.0583" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="26" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="71" value="" style="edgeStyle=none;html=1;entryX=-0.012;entryY=0.384;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-530" y="760" as="sourcePoint"/>
|
||||
<mxPoint x="115.00000000000031" y="759.9999999999999" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="72" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><font color="#a31515">Request with OKTA mechanism</font></div></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="71">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
|
||||
3
SASjsServer.svg
Normal file
3
SASjsServer.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 37 KiB |
@@ -25,7 +25,7 @@ LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||
|
||||
#default value is 100
|
||||
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
|
||||
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
|
||||
|
||||
#default value is 10
|
||||
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
|
||||
|
||||
16
api/package-lock.json
generated
16
api/package-lock.json
generated
@@ -51,7 +51,7 @@
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "8.11.4",
|
||||
@@ -4788,12 +4788,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
|
||||
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer2": {
|
||||
@@ -15217,9 +15217,9 @@
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
|
||||
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"duplexer2": {
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "8.11.4",
|
||||
|
||||
@@ -98,17 +98,47 @@ components:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'Code of program'
|
||||
example: '* Code HERE;'
|
||||
description: 'The code to be executed'
|
||||
example: '* Your Code HERE;'
|
||||
runTime:
|
||||
$ref: '#/components/schemas/RunTimeType'
|
||||
description: 'runtime for program'
|
||||
description: 'The runtime for the code - eg SAS, JS, PY or R'
|
||||
example: js
|
||||
required:
|
||||
- code
|
||||
- runTime
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TriggerCodeResponse:
|
||||
properties:
|
||||
sessionId:
|
||||
type: string
|
||||
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store code outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
|
||||
example: 20241028074744-54132-1730101664824
|
||||
required:
|
||||
- sessionId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TriggerCodePayload:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'The code to be executed'
|
||||
example: '* Your Code HERE;'
|
||||
runTime:
|
||||
$ref: '#/components/schemas/RunTimeType'
|
||||
description: 'The runtime for the code - eg SAS, JS, PY or R'
|
||||
example: sas
|
||||
expiresAfterMins:
|
||||
type: number
|
||||
format: double
|
||||
description: "Amount of minutes after the completion of the job when the session must be\ndestroyed."
|
||||
example: 15
|
||||
required:
|
||||
- code
|
||||
- runTime
|
||||
type: object
|
||||
additionalProperties: false
|
||||
MemberType.folder:
|
||||
enum:
|
||||
- folder
|
||||
@@ -555,6 +585,14 @@ components:
|
||||
- needsToUpdatePassword
|
||||
type: object
|
||||
additionalProperties: false
|
||||
SessionState:
|
||||
enum:
|
||||
- initialising
|
||||
- pending
|
||||
- running
|
||||
- completed
|
||||
- failed
|
||||
type: string
|
||||
ExecutePostRequestPayload:
|
||||
properties:
|
||||
_program:
|
||||
@@ -563,6 +601,16 @@ components:
|
||||
example: /Public/somefolder/some.file
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TriggerProgramResponse:
|
||||
properties:
|
||||
sessionId:
|
||||
type: string
|
||||
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store program outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
|
||||
example: 20241028074744-54132-1730101664824
|
||||
required:
|
||||
- sessionId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
LoginPayload:
|
||||
properties:
|
||||
username:
|
||||
@@ -792,7 +840,7 @@ paths:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: 'Execute Code on the Specified Runtime'
|
||||
summary: 'Run Code and Return Webout Content and Log'
|
||||
summary: "Run Code and Return Webout Content, Log and Print output\nThe order of returned parts of the payload is:\n1. Webout (if present)\n2. Logs UUID (used as separator)\n3. Log\n4. Logs UUID (used as separator)\n5. Print (if present and if the runtime is SAS)\nPlease see"
|
||||
tags:
|
||||
- Code
|
||||
security:
|
||||
@@ -805,6 +853,30 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExecuteCodePayload'
|
||||
/SASjsApi/code/trigger:
|
||||
post:
|
||||
operationId: TriggerCode
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TriggerCodeResponse'
|
||||
description: 'Trigger Code on the Specified Runtime'
|
||||
summary: 'Triggers code and returns SessionId immediately - does not wait for job completion'
|
||||
tags:
|
||||
- Code
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TriggerCodePayload'
|
||||
/SASjsApi/drive/deploy:
|
||||
post:
|
||||
operationId: Deploy
|
||||
@@ -1777,6 +1849,30 @@ paths:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
'/SASjsApi/session/{sessionId}/state':
|
||||
get:
|
||||
operationId: SessionState
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SessionState'
|
||||
description: "The polling endpoint is currently implemented for single-server deployments only.<br>\nLoad balanced / grid topologies will be supported in a future release.<br>\nIf your site requires this, please reach out to SASjs Support."
|
||||
summary: 'Get session state (initialising, pending, running, completed, failed).'
|
||||
tags:
|
||||
- Session
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
in: path
|
||||
name: sessionId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
/SASjsApi/stp/execute:
|
||||
get:
|
||||
operationId: ExecuteGetRequest
|
||||
@@ -1789,7 +1885,7 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts additional URL parameters (converted to session variables)\nand file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
|
||||
tags:
|
||||
- STP
|
||||
@@ -1798,13 +1894,22 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of code in SASjs Drive'
|
||||
description: 'Location of Stored Program in SASjs Drive.'
|
||||
in: query
|
||||
name: _program
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Projects/myApp/some/program
|
||||
-
|
||||
description: 'Optional query param for setting debug mode (returns the session log in the response body).'
|
||||
in: query
|
||||
name: _debug
|
||||
required: false
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 131
|
||||
post:
|
||||
operationId: ExecutePostRequest
|
||||
responses:
|
||||
@@ -1838,6 +1943,50 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExecutePostRequestPayload'
|
||||
/SASjsApi/stp/trigger:
|
||||
post:
|
||||
operationId: TriggerProgram
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TriggerProgramResponse'
|
||||
description: 'Trigger Program on the Specified Runtime.'
|
||||
summary: 'Triggers program and returns SessionId immediately - does not wait for program completion.'
|
||||
tags:
|
||||
- STP
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of code in SASjs Drive.'
|
||||
in: query
|
||||
name: _program
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Projects/myApp/some/program
|
||||
-
|
||||
description: 'Optional query param for setting debug mode.'
|
||||
in: query
|
||||
name: _debug
|
||||
required: false
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 131
|
||||
-
|
||||
description: 'Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.'
|
||||
in: query
|
||||
name: expiresAfterMins
|
||||
required: false
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 15
|
||||
/:
|
||||
get:
|
||||
operationId: Home
|
||||
|
||||
@@ -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')
|
||||
export class CodeController {
|
||||
/**
|
||||
* Execute Code on the Specified Runtime
|
||||
* @summary Run Code and Return Webout Content and Log
|
||||
* @summary Run Code and Return Webout Content, Log and Print output
|
||||
* The order of returned parts of the payload is:
|
||||
* 1. Webout (if present)
|
||||
* 2. Logs UUID (used as separator)
|
||||
* 3. Log
|
||||
* 4. Logs UUID (used as separator)
|
||||
* 5. Print (if present and if the runtime is SAS)
|
||||
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
|
||||
*/
|
||||
@Post('/execute')
|
||||
public async executeCode(
|
||||
@@ -37,6 +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,7 +104,8 @@ const executeCode = async (
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: { ...req.query, _debug: 131 },
|
||||
otherArgs: { userAutoExec },
|
||||
runTime: runTime
|
||||
runTime: runTime,
|
||||
includePrintOutput: true
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -68,3 +118,49 @@ const executeCode = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
status: 'failure',
|
||||
message: 'Job execution failed.',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -33,6 +33,7 @@ interface ExecuteFileParams {
|
||||
|
||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||
program: string
|
||||
includePrintOutput?: boolean
|
||||
}
|
||||
|
||||
export class ExecutionController {
|
||||
@@ -67,18 +68,17 @@ export class ExecutionController {
|
||||
otherArgs,
|
||||
session: sessionByFileUpload,
|
||||
runTime,
|
||||
forceStringResult
|
||||
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')
|
||||
|
||||
const weboutPath = path.join(session.path, 'webout.txt')
|
||||
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||
|
||||
@@ -120,13 +120,32 @@ export class ExecutionController {
|
||||
: ''
|
||||
|
||||
// it should be deleted by scheduleSessionDestroy
|
||||
session.inUse = false
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import { Session } from '../../types'
|
||||
import { Session, SessionState } from '../../types'
|
||||
import { promisify } from 'util'
|
||||
import { execFile } from 'child_process'
|
||||
import {
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
isWindows
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
@@ -24,7 +23,9 @@ export class SessionController {
|
||||
protected sessions: Session[] = []
|
||||
|
||||
protected getReadySessions = (): Session[] =>
|
||||
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||
this.sessions.filter(
|
||||
(session: Session) => session.state === SessionState.pending
|
||||
)
|
||||
|
||||
protected async createSession(): Promise<Session> {
|
||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||
@@ -40,19 +41,18 @@ export class SessionController {
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
ready: true,
|
||||
inUse: true,
|
||||
consumed: false,
|
||||
completed: false,
|
||||
state: SessionState.pending,
|
||||
creationTimeStamp,
|
||||
deathTimeStamp,
|
||||
path: sessionFolder
|
||||
}
|
||||
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
|
||||
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
|
||||
|
||||
this.sessions.push(session)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
@@ -67,6 +67,10 @@ export class SessionController {
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
public getSessionById(id: string) {
|
||||
return this.sessions.find((session) => session.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
export class SASSessionController extends SessionController {
|
||||
@@ -84,10 +88,7 @@ export class SASSessionController extends SessionController {
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
ready: false,
|
||||
inUse: false,
|
||||
consumed: false,
|
||||
completed: false,
|
||||
state: SessionState.initialising,
|
||||
creationTimeStamp,
|
||||
deathTimeStamp,
|
||||
path: sessionFolder
|
||||
@@ -145,13 +146,20 @@ ${autoExecContent}`
|
||||
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
||||
])
|
||||
.then(() => {
|
||||
session.completed = true
|
||||
session.state = SessionState.completed
|
||||
|
||||
process.logger.info('session completed', session)
|
||||
})
|
||||
.catch((err) => {
|
||||
session.completed = true
|
||||
session.crashed = err.toString()
|
||||
process.logger.error('session crashed', session.id, session.crashed)
|
||||
session.state = SessionState.failed
|
||||
|
||||
session.failureReason = err.toString()
|
||||
|
||||
process.logger.error(
|
||||
'session crashed',
|
||||
session.id,
|
||||
session.failureReason
|
||||
)
|
||||
})
|
||||
|
||||
// we have a triggered session - add to array
|
||||
@@ -168,15 +176,19 @@ ${autoExecContent}`
|
||||
const codeFilePath = path.join(session.path, 'code.sas')
|
||||
|
||||
// TODO: don't wait forever
|
||||
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
||||
while (
|
||||
(await fileExists(codeFilePath)) &&
|
||||
session.state !== SessionState.failed
|
||||
) {}
|
||||
|
||||
if (session.crashed)
|
||||
if (session.state === SessionState.failed) {
|
||||
process.logger.error(
|
||||
'session crashed! while waiting to be ready',
|
||||
session.crashed
|
||||
session.failureReason
|
||||
)
|
||||
|
||||
session.ready = true
|
||||
} else {
|
||||
session.state = SessionState.pending
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteSession(session: Session) {
|
||||
@@ -190,17 +202,37 @@ ${autoExecContent}`
|
||||
}
|
||||
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
setTimeout(
|
||||
async () => {
|
||||
if (session.state === SessionState.running) {
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp =
|
||||
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
await this.deleteSession(session)
|
||||
}
|
||||
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
const { expiresAfterMins } = session
|
||||
|
||||
// delay session destroy if expiresAfterMins present
|
||||
if (expiresAfterMins && session.state !== SessionState.completed) {
|
||||
// calculate session death time using expiresAfterMins
|
||||
const newDeathTimeStamp =
|
||||
parseInt(session.deathTimeStamp) +
|
||||
expiresAfterMins.mins * 60 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
// set expiresAfterMins to true to avoid using it again
|
||||
session.expiresAfterMins!.used = true
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
await this.deleteSession(session)
|
||||
}
|
||||
}
|
||||
},
|
||||
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const createJSProgram = async (
|
||||
) => {
|
||||
const varStatments = Object.keys(vars).reduce(
|
||||
(computed: string, key: string) =>
|
||||
`${computed}const ${key} = '${vars[key]}';\n`,
|
||||
`${computed}const ${key} = \`${vars[key]}\`;\n`,
|
||||
''
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WriteStream, createWriteStream } from 'fs'
|
||||
import { execFile } from 'child_process'
|
||||
import { once } from 'stream'
|
||||
import { createFile, moveFile } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { PreProgramVars, Session, SessionState } from '../../types'
|
||||
import { RunTimeType } from '../../utils'
|
||||
import {
|
||||
ExecutionVars,
|
||||
@@ -49,7 +49,7 @@ export const processProgram = async (
|
||||
await moveFile(codePath + '.bkp', codePath)
|
||||
|
||||
// we now need to poll the session status
|
||||
while (!session.completed) {
|
||||
while (session.state !== SessionState.completed) {
|
||||
await delay(50)
|
||||
}
|
||||
} else {
|
||||
@@ -114,13 +114,20 @@ export const processProgram = async (
|
||||
|
||||
await execFilePromise(executablePath, [codePath], writeStream)
|
||||
.then(() => {
|
||||
session.completed = true
|
||||
session.state = SessionState.completed
|
||||
|
||||
process.logger.info('session completed', session)
|
||||
})
|
||||
.catch((err) => {
|
||||
session.completed = true
|
||||
session.crashed = err.toString()
|
||||
process.logger.error('session crashed', session.id, session.crashed)
|
||||
session.state = SessionState.failed
|
||||
|
||||
session.failureReason = err.toString()
|
||||
|
||||
process.logger.error(
|
||||
'session crashed',
|
||||
session.id,
|
||||
session.failureReason
|
||||
)
|
||||
})
|
||||
|
||||
// copy the code file to log and end write stream
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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
|
||||
@@ -26,6 +28,18 @@ export class SessionController {
|
||||
): Promise<SessionResponse> {
|
||||
return session(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* The polling endpoint is currently implemented for single-server deployments only.<br>
|
||||
* Load balanced / grid topologies will be supported in a future release.<br>
|
||||
* If your site requires this, please reach out to SASjs Support.
|
||||
* @summary Get session state (initialising, pending, running, completed, failed).
|
||||
* @example completed
|
||||
*/
|
||||
@Get('/:sessionId/state')
|
||||
public async sessionState(sessionId: string): Promise<SessionState> {
|
||||
return sessionState(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
const session = (req: express.Request) => ({
|
||||
@@ -35,3 +49,23 @@ const session = (req: express.Request) => ({
|
||||
isAdmin: req.user!.isAdmin,
|
||||
needsToUpdatePassword: req.user!.needsToUpdatePassword
|
||||
})
|
||||
|
||||
const sessionState = (sessionId: string): SessionState => {
|
||||
for (let runTime of process.runTimes) {
|
||||
// get session controller for each available runTime
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
// get session by sessionId
|
||||
const session = sessionController.getSessionById(sessionId)
|
||||
|
||||
// return session state if session was found
|
||||
if (session) {
|
||||
return session.state
|
||||
}
|
||||
}
|
||||
|
||||
throw {
|
||||
code: 404,
|
||||
message: `Session with ID '${sessionId}' was not found.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +20,36 @@ interface ExecutePostRequestPayload {
|
||||
_program?: string
|
||||
}
|
||||
|
||||
interface TriggerProgramPayload {
|
||||
/**
|
||||
* Location of SAS program.
|
||||
* @example "/Public/somefolder/some.file"
|
||||
*/
|
||||
_program: string
|
||||
/**
|
||||
* Amount of minutes after the completion of the program when the session must be
|
||||
* destroyed.
|
||||
* @example 15
|
||||
*/
|
||||
expiresAfterMins?: number
|
||||
/**
|
||||
* Query param for setting debug mode.
|
||||
*/
|
||||
_debug?: number
|
||||
}
|
||||
|
||||
interface TriggerProgramResponse {
|
||||
/**
|
||||
* `sessionId` is the ID of the session and the name of the temporary folder
|
||||
* used to store program outputs.<br><br>
|
||||
* For SAS, this would be the location of the SASWORK folder.<br><br>
|
||||
* `sessionId` can be used to poll session state using the
|
||||
* GET /SASjsApi/session/{sessionId}/state endpoint.
|
||||
* @example "20241028074744-54132-1730101664824"
|
||||
*/
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/stp')
|
||||
@Tags('STP')
|
||||
@@ -25,20 +57,31 @@ export class STPController {
|
||||
/**
|
||||
* Trigger a Stored Program using the _program URL parameter.
|
||||
*
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
* Accepts additional URL parameters (converted to session variables)
|
||||
* and file uploads. For more details, see docs:
|
||||
*
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||
* @param _program Location of code in SASjs Drive
|
||||
* @param _program Location of Stored Program in SASjs Drive.
|
||||
* @param _debug Optional query param for setting debug mode (returns the session log in the response body).
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
* @example _debug 131
|
||||
*/
|
||||
@Get('/execute')
|
||||
public async executeGetRequest(
|
||||
@Request() request: express.Request,
|
||||
@Query() _program: string
|
||||
@Query() _program: string,
|
||||
@Query() _debug?: number
|
||||
): Promise<string | Buffer> {
|
||||
const vars = request.query as ExecutionVars
|
||||
let vars = request.query as ExecutionVars
|
||||
if (_debug) {
|
||||
vars = {
|
||||
...vars,
|
||||
_debug
|
||||
}
|
||||
}
|
||||
|
||||
return execute(request, _program, vars)
|
||||
}
|
||||
|
||||
@@ -69,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 (
|
||||
@@ -107,3 +170,52 @@ const execute = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const triggerProgram = async (
|
||||
req: express.Request,
|
||||
{ _program, _debug, expiresAfterMins }: TriggerProgramPayload
|
||||
): Promise<TriggerProgramResponse> => {
|
||||
try {
|
||||
// put _program query param into vars object
|
||||
const vars: { [key: string]: string | number } = { _program }
|
||||
|
||||
// if present add _debug query param to vars object
|
||||
if (_debug) {
|
||||
vars._debug = _debug
|
||||
}
|
||||
|
||||
// get code path and runTime
|
||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
||||
|
||||
// get session controller based on runTime
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
// get session
|
||||
const session = await sessionController.getSession()
|
||||
|
||||
// add expiresAfterMins to session if provided
|
||||
if (expiresAfterMins) {
|
||||
// expiresAfterMins.used is set initially to false
|
||||
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
|
||||
}
|
||||
|
||||
// call executeFile method of ExecutionController without awaiting
|
||||
new ExecutionController().executeFile({
|
||||
programPath: codePath,
|
||||
runTime,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars,
|
||||
session
|
||||
})
|
||||
|
||||
// return session id
|
||||
return { sessionId: session.id }
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
message: 'Job execution failed.',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express'
|
||||
import { runCodeValidation } from '../../utils'
|
||||
import { runCodeValidation, triggerCodeValidation } from '../../utils'
|
||||
import { CodeController } from '../../controllers/'
|
||||
|
||||
const runRouter = express.Router()
|
||||
@@ -28,4 +28,22 @@ runRouter.post('/execute', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
runRouter.post('/trigger', async (req, res) => {
|
||||
const { error, value: body } = triggerCodeValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.triggerCode(req, body)
|
||||
|
||||
res.status(200)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default runRouter
|
||||
|
||||
@@ -1,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
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
SASSessionController
|
||||
} from '../../../controllers/internal'
|
||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
||||
import { Session } from '../../../types'
|
||||
import { Session, SessionState } from '../../../types'
|
||||
|
||||
const clientId = 'someclientID'
|
||||
|
||||
@@ -493,10 +493,7 @@ const mockedGetSession = async () => {
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
ready: true,
|
||||
inUse: true,
|
||||
consumed: false,
|
||||
completed: false,
|
||||
state: SessionState.pending,
|
||||
creationTimeStamp,
|
||||
deathTimeStamp,
|
||||
path: sessionFolder
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import path from 'path'
|
||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||
|
||||
import {
|
||||
createFolder,
|
||||
getAbsolutePath,
|
||||
getRealPath,
|
||||
fileExists
|
||||
} from '@sasjs/utils'
|
||||
import dotenv from 'dotenv'
|
||||
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
|
||||
|
||||
export const setProcessVariables = async () => {
|
||||
const { execPath } = process
|
||||
|
||||
// Check if execPath ends with 'api-macos' to determine executable for MacOS.
|
||||
// This is needed to fix picking .env file issue in MacOS executable.
|
||||
if (execPath) {
|
||||
const envPathSplitted = execPath.split(path.sep)
|
||||
|
||||
if (envPathSplitted.pop() === 'api-macos') {
|
||||
const envPath = path.join(envPathSplitted.join(path.sep), '.env')
|
||||
|
||||
// Override environment variables from envPath if file exists
|
||||
if (await fileExists(envPath)) {
|
||||
dotenv.config({ path: envPath, override: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { MODE, RUN_TIMES } = process.env
|
||||
|
||||
if (MODE === ModeType.Server) {
|
||||
@@ -21,6 +43,7 @@ export const setProcessVariables = async () => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
|
||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,7 +64,9 @@ export const setProcessVariables = async () => {
|
||||
|
||||
const { SASJS_ROOT } = process.env
|
||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||
|
||||
await createFolder(absPath)
|
||||
|
||||
process.sasjsRoot = getRealPath(absPath)
|
||||
|
||||
const { DRIVE_LOCATION } = process.env
|
||||
@@ -49,6 +74,7 @@ export const setProcessVariables = async () => {
|
||||
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
|
||||
process.cwd()
|
||||
)
|
||||
|
||||
await createFolder(absDrivePath)
|
||||
process.driveLoc = getRealPath(absDrivePath)
|
||||
|
||||
@@ -57,7 +83,9 @@ export const setProcessVariables = async () => {
|
||||
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
|
||||
process.cwd()
|
||||
)
|
||||
|
||||
await createFolder(absLogsPath)
|
||||
|
||||
process.logsLoc = getRealPath(absLogsPath)
|
||||
|
||||
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
|
||||
@@ -51,9 +51,8 @@ export const generateFileUploadSasCode = async (
|
||||
let fileCount = 0
|
||||
const uploadedFiles: UploadedFiles[] = []
|
||||
|
||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||
sasSessionFolder
|
||||
)
|
||||
const sasSessionFolderList: string[] =
|
||||
await listFilesInFolder(sasSessionFolder)
|
||||
sasSessionFolderList.forEach((fileName) => {
|
||||
let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount
|
||||
fileCountString = fileCount < 10 ? '00' + fileCount : fileCount
|
||||
|
||||
@@ -178,9 +178,31 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
runTime: Joi.string().valid(...process.runTimes)
|
||||
}).validate(data)
|
||||
|
||||
export const triggerCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
code: Joi.string().required(),
|
||||
runTime: Joi.string().valid(...process.runTimes),
|
||||
expiresAfterMins: Joi.number().greater(0)
|
||||
}).validate(data)
|
||||
|
||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_program: Joi.string().required()
|
||||
_program: Joi.string().required(),
|
||||
_debug: Joi.number()
|
||||
})
|
||||
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
|
||||
.validate(data)
|
||||
|
||||
export const triggerProgramValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_program: Joi.string().required(),
|
||||
_debug: Joi.number(),
|
||||
expiresAfterMins: Joi.number().greater(0)
|
||||
})
|
||||
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
|
||||
.validate(data)
|
||||
|
||||
export const sessionIdValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
sessionId: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
@@ -3,12 +3,11 @@ import Snackbar from '@mui/material/Snackbar'
|
||||
import MuiAlert, { AlertProps } from '@mui/material/Alert'
|
||||
import Slide, { SlideProps } from '@mui/material/Slide'
|
||||
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
})
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||
function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
}
|
||||
)
|
||||
|
||||
const Transition = (props: SlideProps) => {
|
||||
return <Slide {...props} direction="up" />
|
||||
|
||||
@@ -62,6 +62,7 @@ const SASjsEditor = ({
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
printOutput,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
@@ -153,30 +154,35 @@ const SASjsEditor = ({
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<StyledTab label="Code" value="code" />
|
||||
<StyledTab
|
||||
label={logWithErrorsOrWarnings ? '' : 'log'}
|
||||
value="log"
|
||||
icon={
|
||||
logWithErrorsOrWarnings ? (
|
||||
<LogTabWithIcons log={log as LogObject} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const logWrapper = document.querySelector(`#logWrapper`)
|
||||
{log && (
|
||||
<StyledTab
|
||||
label={logWithErrorsOrWarnings ? '' : 'log'}
|
||||
value="log"
|
||||
icon={
|
||||
logWithErrorsOrWarnings ? (
|
||||
<LogTabWithIcons log={log as LogObject} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const logWrapper = document.querySelector(`#logWrapper`)
|
||||
|
||||
if (logWrapper) logWrapper.scrollTop = 0
|
||||
}}
|
||||
/>
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Typography>Webout</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
value="webout"
|
||||
/>
|
||||
if (logWrapper) logWrapper.scrollTop = 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{webout && (
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Typography>Webout</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
value="webout"
|
||||
/>
|
||||
)}
|
||||
{printOutput && <StyledTab label="print" value="printOutput" />}
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
@@ -222,11 +228,20 @@ const SASjsEditor = ({
|
||||
<LogComponent log={log} selectedRunTime={selectedRunTime} />
|
||||
)}
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
{webout && (
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
)}
|
||||
{printOutput && (
|
||||
<StyledTabPanel value="printOutput">
|
||||
<div>
|
||||
<pre>{printOutput}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
)}
|
||||
</TabContext>
|
||||
)}
|
||||
<Dialog />
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: 0.4s;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px,
|
||||
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.2) 0px 2px 1px -1px,
|
||||
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px,
|
||||
rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
|
||||
}
|
||||
|
||||
.ChunkDetails {
|
||||
|
||||
@@ -39,14 +39,14 @@ const useEditor = ({
|
||||
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||
useSnackbar()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState<LogObject | string>()
|
||||
const [webout, setWebout] = useState('')
|
||||
const [webout, setWebout] = useState<string>()
|
||||
const [printOutput, setPrintOutput] = useState<string>()
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType | string>(
|
||||
''
|
||||
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType>(
|
||||
RunTimeType.SAS
|
||||
)
|
||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||
@@ -169,25 +169,30 @@ const useEditor = ({
|
||||
),
|
||||
runTime: selectedRunTime
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (selectedRunTime === RunTimeType.SAS) {
|
||||
const { errors, warnings, logLines } = parseErrorsAndWarnings(
|
||||
res.data.split(SASJS_LOGS_SEPARATOR)[1]
|
||||
)
|
||||
.then((res: { data: string }) => {
|
||||
// INFO: the order of payload parts is set in @sasjs/server/api/src/controllers/internal/Execution.ts
|
||||
const resDataSplitted = res.data.split(SASJS_LOGS_SEPARATOR)
|
||||
const webout = resDataSplitted[0]
|
||||
const log = resDataSplitted[1]
|
||||
const printOutput = resDataSplitted[2]
|
||||
|
||||
const log: LogObject = {
|
||||
if (selectedRunTime === RunTimeType.SAS) {
|
||||
const { errors, warnings, logLines } = parseErrorsAndWarnings(log)
|
||||
|
||||
const logObject: LogObject = {
|
||||
body: logLines.join(`\n`),
|
||||
errors,
|
||||
warnings,
|
||||
linesCount: logLines.length
|
||||
}
|
||||
|
||||
setLog(log)
|
||||
setLog(logObject)
|
||||
} else {
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
setLog(log)
|
||||
}
|
||||
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setWebout(webout)
|
||||
setPrintOutput(printOutput)
|
||||
setTab('log')
|
||||
|
||||
// Scroll to bottom of log
|
||||
@@ -335,6 +340,7 @@ const useEditor = ({
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
printOutput,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
Reference in New Issue
Block a user