mirror of
https://github.com/sasjs/server.git
synced 2025-12-08 02:42:44 +00:00
Compare commits
13 Commits
ebaffc62a6
...
cb2faee5a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb2faee5a7 | ||
|
|
c261745f1d | ||
|
|
d6e527ecf2 | ||
|
|
bc2cff1d0d | ||
|
|
66aa9b5891 | ||
|
|
ca17e7c192 | ||
|
|
73df102422 | ||
|
|
48a9a4dd0e | ||
|
|
4f6f735f5b | ||
|
|
6b6546c7ad | ||
|
|
f94ddc0352 | ||
|
|
03670cf0d6 | ||
|
|
b03a5db22f |
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"autoexec"
|
||||
]
|
||||
"cSpell.words": ["autoexec", "initialising"]
|
||||
}
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,3 +1,15 @@
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -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 |
@@ -113,8 +113,8 @@ components:
|
||||
properties:
|
||||
sessionId:
|
||||
type: string
|
||||
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll job status.\nThis session ID should be used to poll job status."
|
||||
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
|
||||
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
|
||||
@@ -585,6 +585,14 @@ components:
|
||||
- needsToUpdatePassword
|
||||
type: object
|
||||
additionalProperties: false
|
||||
SessionState:
|
||||
enum:
|
||||
- initialising
|
||||
- pending
|
||||
- running
|
||||
- completed
|
||||
- failed
|
||||
type: string
|
||||
ExecutePostRequestPayload:
|
||||
properties:
|
||||
_program:
|
||||
@@ -597,8 +605,8 @@ components:
|
||||
properties:
|
||||
sessionId:
|
||||
type: string
|
||||
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll program status.\nThis session ID should be used to poll program status."
|
||||
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
|
||||
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
|
||||
@@ -1841,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
|
||||
|
||||
@@ -42,10 +42,12 @@ interface TriggerCodePayload {
|
||||
|
||||
interface TriggerCodeResponse {
|
||||
/**
|
||||
* The SessionId is the name of the temporary folder used to store the outputs.
|
||||
* For SAS, this would be the SASWORK folder. Can be used to poll job status.
|
||||
* This session ID should be used to poll job status.
|
||||
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
|
||||
* `sessionId` 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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -75,8 +75,7 @@ export class ExecutionController {
|
||||
|
||||
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')
|
||||
@@ -121,7 +120,7 @@ export class ExecutionController {
|
||||
: ''
|
||||
|
||||
// it should be deleted by scheduleSessionDestroy
|
||||
session.inUse = false
|
||||
session.state = SessionState.completed
|
||||
|
||||
const resultParts = []
|
||||
|
||||
@@ -145,7 +144,9 @@ export class ExecutionController {
|
||||
return {
|
||||
httpHeaders,
|
||||
result:
|
||||
isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout
|
||||
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 {
|
||||
@@ -23,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())
|
||||
@@ -39,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
|
||||
}
|
||||
|
||||
@@ -66,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 {
|
||||
@@ -83,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
|
||||
@@ -144,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
|
||||
@@ -167,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) {
|
||||
@@ -191,7 +204,7 @@ ${autoExecContent}`
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(
|
||||
async () => {
|
||||
if (session.inUse) {
|
||||
if (session.state === SessionState.running) {
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp =
|
||||
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
|
||||
@@ -202,7 +215,7 @@ ${autoExecContent}`
|
||||
const { expiresAfterMins } = session
|
||||
|
||||
// delay session destroy if expiresAfterMins present
|
||||
if (expiresAfterMins && !expiresAfterMins.used) {
|
||||
if (expiresAfterMins && session.state !== SessionState.completed) {
|
||||
// calculate session death time using expiresAfterMins
|
||||
const newDeathTimeStamp =
|
||||
parseInt(session.deathTimeStamp) +
|
||||
|
||||
@@ -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.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,12 @@ interface TriggerProgramPayload {
|
||||
|
||||
interface TriggerProgramResponse {
|
||||
/**
|
||||
* The SessionId is the name of the temporary folder used to store the outputs.
|
||||
* For SAS, this would be the SASWORK folder. Can be used to poll program status.
|
||||
* This session ID should be used to poll program status.
|
||||
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
|
||||
* `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
|
||||
}
|
||||
|
||||
@@ -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,12 +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
|
||||
}
|
||||
|
||||
@@ -201,3 +201,8 @@ export const triggerProgramValidation = (data: any): Joi.ValidationResult =>
|
||||
})
|
||||
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
|
||||
.validate(data)
|
||||
|
||||
export const sessionIdValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
sessionId: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
Reference in New Issue
Block a user