mirror of
https://github.com/sasjs/server.git
synced 2025-12-17 14:04:36 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a131adbae7 | ||
|
|
a20c3b9719 | ||
|
|
eee3a7b084 | ||
|
|
9c3da56901 | ||
|
|
7e6524d7e4 | ||
|
|
0ea2690616 | ||
|
|
b369759f0f | ||
|
|
ac9a835c5a | ||
|
|
e290751c87 | ||
|
|
71bcbb9134 | ||
|
|
c86f0feff8 | ||
|
|
d3d2ab9a36 | ||
| 5cc85b57f8 | |||
|
|
ae0fc0c48c | ||
|
|
555c5d54e2 | ||
| 1b5859ee37 | |||
| 65380be2f3 | |||
|
|
1933be15c2 | ||
|
|
56b20beb8c | ||
|
|
bfc5ac6a4f | ||
|
|
6376173de0 | ||
|
|
3130fbeff0 | ||
|
|
01e9a1d9e9 | ||
|
|
2119e9de9a | ||
|
|
87dbab98f6 | ||
|
|
1bf122a0a2 | ||
|
|
5d5d6ce326 | ||
|
|
620eddb713 | ||
|
|
3c92034da3 | ||
|
|
f6dc74f16b | ||
|
|
8c48d00d21 | ||
|
|
48ff8d73d4 | ||
| eb397b15c2 | |||
| eb569c7b82 | |||
| 99a1107364 | |||
| 91d29cb127 |
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,3 +1,62 @@
|
|||||||
|
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* removed secrets from env variables ([9c3da56](https://github.com/sasjs/server/commit/9c3da56901672a818f54267f9defc9f4701ab7fb))
|
||||||
|
|
||||||
|
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deploy:** extract first json from zip file ([e290751](https://github.com/sasjs/server/commit/e290751c872d24009482871a8c398e834357dcde))
|
||||||
|
|
||||||
|
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](https://github.com/sasjs/server/commit/5cc85b57f80b13296156811fe966d7b37d45f213))
|
||||||
|
|
||||||
|
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* make CA_ROOT optional in getCertificates method ([1b5859e](https://github.com/sasjs/server/commit/1b5859ee37ae73c419115b9debfd5141a79733de))
|
||||||
|
* update /logout route to /SASLogon/logout ([65380be](https://github.com/sasjs/server/commit/65380be2f3945bae559f1749064845b514447a53))
|
||||||
|
|
||||||
|
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](https://github.com/sasjs/server/commit/2119e9de9ab1e5ce1222658f554ac74f4f35cf4d))
|
||||||
|
|
||||||
|
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* path descriptions and defaults ([5d5d6ce](https://github.com/sasjs/server/commit/5d5d6ce3265a43af2e22bcd38cda54fafaf7b3ef))
|
||||||
|
|
||||||
|
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing UTF-8 options from commandline. There appears to be no reliable way to enforce ([f6dc74f](https://github.com/sasjs/server/commit/f6dc74f16bddafa1de9c83c2f27671a241abdad4))
|
||||||
|
|
||||||
|
## [0.7.1](https://github.com/sasjs/server/compare/v0.7.0...v0.7.1) (2022-06-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* default runtime should be sas ([91d29cb](https://github.com/sasjs/server/commit/91d29cb1272c28afbceaf39d1e0a87e17fbfdcd6))
|
||||||
|
* **Studio:** default selection of runtime fixed ([eb569c7](https://github.com/sasjs/server/commit/eb569c7b827c872ed2c4bc114559b97d87fd2aa0))
|
||||||
|
* webout path fixed in code.js when running on windows ([99a1107](https://github.com/sasjs/server/commit/99a110736448f66f99a512396b268fc31a3feef0))
|
||||||
|
|
||||||
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)
|
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
19
PULL_REQUEST_TEMPLATE.md
Normal file
19
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Issue
|
||||||
|
|
||||||
|
Link any related issue(s) in this section.
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
|
||||||
|
What this PR intends to achieve.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
What code changes have been made to achieve the intent.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||||
|
- [ ] Any new functionality has been unit tested.
|
||||||
|
- [ ] All unit tests are passing (`npm test`).
|
||||||
|
- [ ] All CI checks are green.
|
||||||
|
- [ ] Reviewer is assigned.
|
||||||
15
README.md
15
README.md
@@ -99,15 +99,12 @@ SASV9_OPTIONS= -NOXCMD
|
|||||||
## Additional Web Server Options
|
## Additional Web Server Options
|
||||||
#
|
#
|
||||||
|
|
||||||
# ENV variables required for PROTOCOL: `https`
|
# ENV variables for PROTOCOL: `https`
|
||||||
PRIVATE_KEY=privkey.pem
|
PRIVATE_KEY=privkey.pem (required)
|
||||||
FULL_CHAIN=fullchain.pem
|
CERT_CHAIN=certificate.pem (required)
|
||||||
|
CA_ROOT=fullchain.pem (optional)
|
||||||
|
|
||||||
# ENV variables required for MODE: `server`
|
# ENV variables required for MODE: `server`
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
|
||||||
AUTH_CODE_SECRET=<secret>
|
|
||||||
SESSION_SECRET=<secret>
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
@@ -140,10 +137,10 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
|
|||||||
LOG_FORMAT_MORGAN=
|
LOG_FORMAT_MORGAN=
|
||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
# A comma separated string that defines the available runTimes.
|
||||||
# Priority is given to the runtime that cSAS_PATHomes first in string.
|
# Priority is given to the runtime that comes first in the string.
|
||||||
# Possible options at the moment are sas and js
|
# Possible options at the moment are sas and js
|
||||||
|
|
||||||
# options: [sas,js|js,sas|sas|js] default:sas,js
|
# options: [sas,js|js,sas|sas|js] default:sas
|
||||||
RUN_TIMES=
|
RUN_TIMES=
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,20 +4,17 @@ WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
|||||||
|
|
||||||
PROTOCOL=[http|https] default considered as http
|
PROTOCOL=[http|https] default considered as http
|
||||||
PRIVATE_KEY=privkey.pem
|
PRIVATE_KEY=privkey.pem
|
||||||
FULL_CHAIN=fullchain.pem
|
CERT_CHAIN=certificate.pem
|
||||||
|
CA_ROOT=fullchain.pem
|
||||||
|
|
||||||
PORT=[5000] default value is 5000
|
PORT=[5000] default value is 5000
|
||||||
|
|
||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
||||||
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||||
|
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
|
||||||
AUTH_CODE_SECRET=<secret>
|
|
||||||
SESSION_SECRET=<secret>
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas,js
|
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
|
||||||
|
|||||||
@@ -47,41 +47,6 @@ components:
|
|||||||
- userId
|
- userId
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
LoginPayload:
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: 'Username for user'
|
|
||||||
example: secretuser
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
description: 'Password for user'
|
|
||||||
example: secretpassword
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
- password
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizeResponse:
|
|
||||||
properties:
|
|
||||||
code:
|
|
||||||
type: string
|
|
||||||
description: 'Authorization code'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
required:
|
|
||||||
- code
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizePayload:
|
|
||||||
properties:
|
|
||||||
clientId:
|
|
||||||
type: string
|
|
||||||
description: 'Client ID'
|
|
||||||
example: clientID1
|
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
ClientPayload:
|
ClientPayload:
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
@@ -144,7 +109,7 @@ components:
|
|||||||
- sas
|
- sas
|
||||||
- js
|
- js
|
||||||
type: string
|
type: string
|
||||||
ExecuteSASCodePayload:
|
ExecuteCodePayload:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
type: string
|
type: string
|
||||||
@@ -440,13 +405,13 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
|
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
|
||||||
properties:
|
properties:
|
||||||
id:
|
|
||||||
description: 'The string version of this documents _id.'
|
|
||||||
_id:
|
_id:
|
||||||
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
|
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
|
||||||
description: 'This documents _id.'
|
description: 'This documents _id.'
|
||||||
__v:
|
__v:
|
||||||
description: 'This documents __v.'
|
description: 'This documents __v.'
|
||||||
|
id:
|
||||||
|
description: 'The string version of this documents _id.'
|
||||||
type: object
|
type: object
|
||||||
description: 'From T, pick a set of properties whose keys are in the union K'
|
description: 'From T, pick a set of properties whose keys are in the union K'
|
||||||
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
|
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
|
||||||
@@ -488,6 +453,41 @@ components:
|
|||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
LoginPayload:
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: 'Username for user'
|
||||||
|
example: secretuser
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: 'Password for user'
|
||||||
|
example: secretpassword
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizeResponse:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Authorization code'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizePayload:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
description: 'Client ID'
|
||||||
|
example: clientID1
|
||||||
|
required:
|
||||||
|
- clientId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -558,86 +558,6 @@ paths:
|
|||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters: []
|
||||||
/:
|
|
||||||
get:
|
|
||||||
operationId: Home
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
summary: 'Render index.html'
|
|
||||||
tags:
|
|
||||||
- Web
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
/SASLogon/login:
|
|
||||||
post:
|
|
||||||
operationId: Login
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
properties:
|
|
||||||
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
|
||||||
loggedIn: {type: boolean}
|
|
||||||
required:
|
|
||||||
- user
|
|
||||||
- loggedIn
|
|
||||||
type: object
|
|
||||||
summary: 'Accept a valid username/password'
|
|
||||||
tags:
|
|
||||||
- Web
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/LoginPayload'
|
|
||||||
/SASLogon/authorize:
|
|
||||||
post:
|
|
||||||
operationId: Authorize
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizeResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {code: someRandomCryptoString}
|
|
||||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
|
||||||
tags:
|
|
||||||
- Web
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizePayload'
|
|
||||||
/logout:
|
|
||||||
get:
|
|
||||||
operationId: Logout
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema: {}
|
|
||||||
summary: 'Accept a valid username/password'
|
|
||||||
tags:
|
|
||||||
- Web
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
/SASjsApi/client:
|
/SASjsApi/client:
|
||||||
post:
|
post:
|
||||||
operationId: CreateClient
|
operationId: CreateClient
|
||||||
@@ -666,7 +586,7 @@ paths:
|
|||||||
$ref: '#/components/schemas/ClientPayload'
|
$ref: '#/components/schemas/ClientPayload'
|
||||||
/SASjsApi/code/execute:
|
/SASjsApi/code/execute:
|
||||||
post:
|
post:
|
||||||
operationId: ExecuteSASCode
|
operationId: ExecuteCode
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Ok
|
description: Ok
|
||||||
@@ -687,7 +607,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteSASCodePayload'
|
$ref: '#/components/schemas/ExecuteCodePayload'
|
||||||
/SASjsApi/drive/deploy:
|
/SASjsApi/drive/deploy:
|
||||||
post:
|
post:
|
||||||
operationId: Deploy
|
operationId: Deploy
|
||||||
@@ -1504,6 +1424,86 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
operationId: Home
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: 'Render index.html'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
/SASLogon/login:
|
||||||
|
post:
|
||||||
|
operationId: Login
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
||||||
|
loggedIn: {type: boolean}
|
||||||
|
required:
|
||||||
|
- user
|
||||||
|
- loggedIn
|
||||||
|
type: object
|
||||||
|
summary: 'Accept a valid username/password'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LoginPayload'
|
||||||
|
/SASLogon/authorize:
|
||||||
|
post:
|
||||||
|
operationId: Authorize
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizeResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {code: someRandomCryptoString}
|
||||||
|
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizePayload'
|
||||||
|
/SASLogon/logout:
|
||||||
|
get:
|
||||||
|
operationId: Logout
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
summary: 'Destroy the session stored in cookies'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
servers:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
import csrf from 'csurf'
|
import csrf from 'csurf'
|
||||||
import session from 'express-session'
|
import session from 'express-session'
|
||||||
import MongoStore from 'connect-mongo'
|
import MongoStore from 'connect-mongo'
|
||||||
@@ -97,45 +98,44 @@ if (CORS === CorsType.ENABLED) {
|
|||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/***********************************
|
export default setProcessVariables().then(async () => {
|
||||||
|
/***********************************
|
||||||
* DB Connection & *
|
* DB Connection & *
|
||||||
* Express Sessions *
|
* Express Sessions *
|
||||||
* With Mongo Store *
|
* With Mongo Store *
|
||||||
***********************************/
|
***********************************/
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
let store: MongoStore | undefined
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
|
||||||
// we should exclude connecting to the real database
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
store = MongoStore.create({
|
||||||
|
client: mongoose.connection!.getClient() as any,
|
||||||
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
collectionName: 'sessions'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
secret: process.env.SESSION_SECRET as string,
|
secret: process.secrets.SESSION_SECRET,
|
||||||
saveUninitialized: false, // don't create session until something stored
|
saveUninitialized: false, // don't create session until something stored
|
||||||
resave: false, //don't save session if unmodified
|
resave: false, //don't save session if unmodified
|
||||||
store,
|
store,
|
||||||
cookie: cookieOptions
|
cookie: cookieOptions
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.json({ limit: '100mb' }))
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
if (err.code === 'EBADCSRFTOKEN')
|
if (err.code === 'EBADCSRFTOKEN')
|
||||||
return res.status(400).send('Invalid CSRF token!')
|
return res.status(400).send('Invalid CSRF token!')
|
||||||
|
|
||||||
console.error(err.stack)
|
console.error(err.stack)
|
||||||
res.status(500).send('Something broke!')
|
res.status(500).send('Something broke!')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default setProcessVariables().then(async () => {
|
|
||||||
await setupFolders()
|
await setupFolders()
|
||||||
await copySASjsCore()
|
await copySASjsCore()
|
||||||
|
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ const verifyAuthCode = async (
|
|||||||
clientId: string,
|
clientId: string,
|
||||||
code: string
|
code: string
|
||||||
): Promise<InfoJWT | undefined> => {
|
): Promise<InfoJWT | undefined> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
|
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
|
||||||
if (err) return resolve(undefined)
|
if (err) return resolve(undefined)
|
||||||
|
|
||||||
const clientInfo: InfoJWT = {
|
const clientInfo: InfoJWT = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface GroupResponse {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupDetailsResponse {
|
export interface GroupDetailsResponse {
|
||||||
groupId: number
|
groupId: number
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -249,9 +249,10 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'User not found.'
|
message: 'User not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedGroup = (action === 'addUser'
|
const updatedGroup =
|
||||||
? await group.addUser(user._id)
|
action === 'addUser'
|
||||||
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
|
? await group.addUser(user)
|
||||||
|
: await group.removeUser(user)
|
||||||
|
|
||||||
if (!updatedGroup)
|
if (!updatedGroup)
|
||||||
throw {
|
throw {
|
||||||
@@ -260,9 +261,6 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'Unable to update group.'
|
message: 'Unable to update group.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'addUser') user.addGroup(group._id)
|
|
||||||
else user.removeGroup(group._id)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: updatedGroup.groupId,
|
groupId: updatedGroup.groupId,
|
||||||
name: updatedGroup.name,
|
name: updatedGroup.name,
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import {
|
import { getSessionController, processProgram } from './'
|
||||||
getSASSessionController,
|
|
||||||
getJSSessionController,
|
|
||||||
processProgram
|
|
||||||
} from './'
|
|
||||||
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
|
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||||
import {
|
import {
|
||||||
@@ -76,10 +72,7 @@ export class ExecutionController {
|
|||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||||
const sessionController =
|
const sessionController = getSessionController(runTime)
|
||||||
runTime === RunTimeType.SAS
|
|
||||||
? getSASSessionController()
|
|
||||||
: getJSSessionController()
|
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
sessionByFileUpload ?? (await sessionController.getSession())
|
sessionByFileUpload ?? (await sessionController.getSession())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, RequestHandler } from 'express'
|
import { Request, RequestHandler } from 'express'
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import { uuidv4 } from '@sasjs/utils'
|
import { uuidv4 } from '@sasjs/utils'
|
||||||
import { getSASSessionController, getJSSessionController } from '.'
|
import { getSessionController } from '.'
|
||||||
import {
|
import {
|
||||||
executeProgramRawValidation,
|
executeProgramRawValidation,
|
||||||
getRunTimeAndFilePath,
|
getRunTimeAndFilePath,
|
||||||
@@ -37,17 +37,23 @@ export class FileUploadController {
|
|||||||
try {
|
try {
|
||||||
;({ runTime } = await getRunTimeAndFilePath(programPath))
|
;({ runTime } = await getRunTimeAndFilePath(programPath))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(400).send({
|
return res.status(400).send({
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'Job execution failed',
|
message: 'Job execution failed',
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionController =
|
let sessionController
|
||||||
runTime === RunTimeType.SAS
|
try {
|
||||||
? getSASSessionController()
|
sessionController = getSessionController(runTime)
|
||||||
: getJSSessionController()
|
} catch (err: any) {
|
||||||
|
return res.status(400).send({
|
||||||
|
status: 'failure',
|
||||||
|
message: err.message,
|
||||||
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const session = await sessionController.getSession()
|
const session = await sessionController.getSession()
|
||||||
// marking consumed true, so that it's not available
|
// marking consumed true, so that it's not available
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { execFile } from 'child_process'
|
|||||||
import {
|
import {
|
||||||
getSessionsFolder,
|
getSessionsFolder,
|
||||||
generateUniqueFileName,
|
generateUniqueFileName,
|
||||||
sysInitCompiledPath
|
sysInitCompiledPath,
|
||||||
|
RunTimeType
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import {
|
import {
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
createFile,
|
createFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
generateTimestamp,
|
generateTimestamp,
|
||||||
readFile
|
readFile,
|
||||||
|
isWindows
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
@@ -88,7 +90,7 @@ ${autoExecContent}`
|
|||||||
|
|
||||||
// Additional windows specific options to avoid the desktop popups.
|
// Additional windows specific options to avoid the desktop popups.
|
||||||
|
|
||||||
execFilePromise(process.sasLoc, [
|
execFilePromise(process.sasLoc!, [
|
||||||
'-SYSIN',
|
'-SYSIN',
|
||||||
codePath,
|
codePath,
|
||||||
'-LOG',
|
'-LOG',
|
||||||
@@ -99,11 +101,9 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
'-ENCODING',
|
isWindows() ? '-nosplash' : '',
|
||||||
'UTF-8',
|
isWindows() ? '-icon' : '',
|
||||||
process.platform === 'win32' ? '-nosplash' : '',
|
isWindows() ? '-nologo' : ''
|
||||||
process.platform === 'win32' ? '-icon' : '',
|
|
||||||
process.platform === 'win32' ? '-nologo' : ''
|
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
@@ -194,7 +194,21 @@ export class JSSessionController extends SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSASSessionController = (): SASSessionController => {
|
export const getSessionController = (
|
||||||
|
runTime: RunTimeType
|
||||||
|
): SASSessionController | JSSessionController => {
|
||||||
|
if (runTime === RunTimeType.SAS) {
|
||||||
|
return getSASSessionController()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runTime === RunTimeType.JS) {
|
||||||
|
return getJSSessionController()
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No Runtime is configured')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSASSessionController = (): SASSessionController => {
|
||||||
if (process.sasSessionController) return process.sasSessionController
|
if (process.sasSessionController) return process.sasSessionController
|
||||||
|
|
||||||
process.sasSessionController = new SASSessionController()
|
process.sasSessionController = new SASSessionController()
|
||||||
@@ -202,7 +216,7 @@ export const getSASSessionController = (): SASSessionController => {
|
|||||||
return process.sasSessionController
|
return process.sasSessionController
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getJSSessionController = (): JSSessionController => {
|
const getJSSessionController = (): JSSessionController => {
|
||||||
if (process.jsSessionController) return process.jsSessionController
|
if (process.jsSessionController) return process.jsSessionController
|
||||||
|
|
||||||
process.jsSessionController = new JSSessionController()
|
process.jsSessionController = new JSSessionController()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isWindows } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session } from '../../types'
|
import { PreProgramVars, Session } from '../../types'
|
||||||
import { generateFileUploadJSCode } from '../../utils'
|
import { generateFileUploadJSCode } from '../../utils'
|
||||||
import { ExecutionVars } from './'
|
import { ExecutionVars } from './'
|
||||||
@@ -19,7 +20,9 @@ export const createJSProgram = async (
|
|||||||
|
|
||||||
const preProgramVarStatments = `
|
const preProgramVarStatments = `
|
||||||
let _webout = '';
|
let _webout = '';
|
||||||
const weboutPath = '${weboutPath}';
|
const weboutPath = '${
|
||||||
|
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||||
|
}';
|
||||||
const _sasjs_tokenfile = '${tokenFile}';
|
const _sasjs_tokenfile = '${tokenFile}';
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _sasjs_username = '${preProgramVariables?.username}';
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _sasjs_userid = '${preProgramVariables?.userId}';
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const processProgram = async (
|
|||||||
// waiting for the open event so that we can have underlying file descriptor
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
await once(writeStream, 'open')
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
execFileSync(process.nodeLoc, [codePath], {
|
execFileSync(process.nodeLoc!, [codePath], {
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
stdio: ['ignore', writeStream, writeStream]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export class WebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Accept a valid username/password
|
* @summary Destroy the session stored in cookies
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Get('/logout')
|
@Get('/SASLogon/logout')
|
||||||
public async logout(@Request() req: express.Request) {
|
public async logout(@Request() req: express.Request) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
req.session.destroy(() => {
|
req.session.destroy(() => {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
process.env.ACCESS_TOKEN_SECRET as string,
|
process.secrets.ACCESS_TOKEN_SECRET,
|
||||||
'accessToken'
|
'accessToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
|||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
process.env.REFRESH_TOKEN_SECRET as string,
|
process.secrets.REFRESH_TOKEN_SECRET,
|
||||||
'refreshToken'
|
'refreshToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
45
api/src/model/Configuration.ts
Normal file
45
api/src/model/Configuration.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import mongoose, { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
export interface ConfigurationType {
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Access Token
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
ACCESS_TOKEN_SECRET: string
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Refresh Token
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
REFRESH_TOKEN_SECRET: string
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Auth Code
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
AUTH_CODE_SECRET: string
|
||||||
|
/**
|
||||||
|
* Secret used to sign the session cookie
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
SESSION_SECRET: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigurationSchema = new Schema<ConfigurationType>({
|
||||||
|
ACCESS_TOKEN_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
REFRESH_TOKEN_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
AUTH_CODE_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
SESSION_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('Configuration', ConfigurationSchema)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
import User from './User'
|
import { GroupDetailsResponse } from '../controllers'
|
||||||
|
import User, { IUser } from './User'
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
|
|
||||||
export interface GroupPayload {
|
export interface GroupPayload {
|
||||||
@@ -27,8 +28,9 @@ interface IGroupDocument extends GroupPayload, Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IGroup extends IGroupDocument {
|
interface IGroup extends IGroupDocument {
|
||||||
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
addUser(user: IUser): Promise<GroupDetailsResponse>
|
||||||
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
removeUser(user: IUser): Promise<GroupDetailsResponse>
|
||||||
|
hasUser(user: IUser): boolean
|
||||||
}
|
}
|
||||||
interface IGroupModel extends Model<IGroup> {}
|
interface IGroupModel extends Model<IGroup> {}
|
||||||
|
|
||||||
@@ -70,28 +72,31 @@ groupSchema.pre('remove', async function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Instance Methods
|
// Instance Methods
|
||||||
groupSchema.method(
|
groupSchema.method('addUser', async function (user: IUser) {
|
||||||
'addUser',
|
const userObjectId = user._id
|
||||||
async function (userObjectId: Schema.Types.ObjectId) {
|
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
if (userIdIndex === -1) {
|
if (userIdIndex === -1) {
|
||||||
this.users.push(userObjectId)
|
this.users.push(userObjectId)
|
||||||
|
user.addGroup(this._id)
|
||||||
}
|
}
|
||||||
this.markModified('users')
|
this.markModified('users')
|
||||||
return this.save()
|
return this.save()
|
||||||
}
|
})
|
||||||
)
|
groupSchema.method('removeUser', async function (user: IUser) {
|
||||||
groupSchema.method(
|
const userObjectId = user._id
|
||||||
'removeUser',
|
|
||||||
async function (userObjectId: Schema.Types.ObjectId) {
|
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
if (userIdIndex > -1) {
|
if (userIdIndex > -1) {
|
||||||
this.users.splice(userIdIndex, 1)
|
this.users.splice(userIdIndex, 1)
|
||||||
|
user.removeGroup(this._id)
|
||||||
}
|
}
|
||||||
this.markModified('users')
|
this.markModified('users')
|
||||||
return this.save()
|
return this.save()
|
||||||
}
|
})
|
||||||
)
|
groupSchema.method('hasUser', function (user: IUser) {
|
||||||
|
const userObjectId = user._id
|
||||||
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
|
return userIdIndex > -1
|
||||||
|
})
|
||||||
|
|
||||||
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
||||||
'Group',
|
'Group',
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface UserPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IUserDocument extends UserPayload, Document {
|
interface IUserDocument extends UserPayload, Document {
|
||||||
|
_id: Schema.Types.ObjectId
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
@@ -43,7 +44,7 @@ interface IUserDocument extends UserPayload, Document {
|
|||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUser extends IUserDocument {
|
export interface IUser extends IUserDocument {
|
||||||
comparePassword(password: string): boolean
|
comparePassword(password: string): boolean
|
||||||
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ webRouter.post(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
webRouter.get('/logout', desktopRestrict, async (req, res) => {
|
webRouter.get('/SASLogon/logout', desktopRestrict, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await controller.logout(req)
|
await controller.logout(req)
|
||||||
res.status(200).send('OK!')
|
res.status(200).send('OK!')
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ appPromise.then(async (app) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const { key, cert } = await getCertificates()
|
const { key, cert, ca } = await getCertificates()
|
||||||
|
|
||||||
const httpsServer = createServer({ key, cert }, app)
|
const httpsServer = createServer({ key, cert, ca }, app)
|
||||||
httpsServer.listen(sasJsPort, () => {
|
httpsServer.listen(sasJsPort, () => {
|
||||||
console.log(
|
console.log(
|
||||||
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
||||||
|
|||||||
5
api/src/types/system/process.d.ts
vendored
5
api/src/types/system/process.d.ts
vendored
@@ -1,12 +1,13 @@
|
|||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
export interface Process {
|
export interface Process {
|
||||||
sasLoc: string
|
sasLoc?: string
|
||||||
nodeLoc: string
|
nodeLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
|
secrets: import('../../model/Configuration').ConfigurationType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,5 @@ export const connectDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Connected to DB!')
|
console.log('Connected to DB!')
|
||||||
await seedDB()
|
return seedDB()
|
||||||
|
|
||||||
return mongoose.connection
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
||||||
expiresIn: '1day'
|
expiresIn: '1day'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAuthCode = (data: InfoJWT) =>
|
export const generateAuthCode = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
|
jwt.sign(data, process.secrets.AUTH_CODE_SECRET, {
|
||||||
expiresIn: '30s'
|
expiresIn: '30s'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
||||||
expiresIn: '30 days'
|
expiresIn: '30 days'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,22 +2,32 @@ import path from 'path'
|
|||||||
import { fileExists, getString, readFile } from '@sasjs/utils'
|
import { fileExists, getString, readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
export const getCertificates = async () => {
|
export const getCertificates = async () => {
|
||||||
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
const { PRIVATE_KEY, CERT_CHAIN, CA_ROOT } = process.env
|
||||||
|
|
||||||
|
let ca
|
||||||
|
|
||||||
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
|
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
|
||||||
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
|
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
|
||||||
|
const caPath = CA_ROOT
|
||||||
|
|
||||||
console.log('keyPath: ', keyPath)
|
console.log('keyPath: ', keyPath)
|
||||||
console.log('certPath: ', certPath)
|
console.log('certPath: ', certPath)
|
||||||
|
if (caPath) console.log('caPath: ', caPath)
|
||||||
|
|
||||||
const key = await readFile(keyPath)
|
const key = await readFile(keyPath)
|
||||||
const cert = await readFile(certPath)
|
const cert = await readFile(certPath)
|
||||||
|
if (caPath) ca = await readFile(caPath)
|
||||||
|
|
||||||
return { key, cert }
|
return { key, cert, ca }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileInput = async (filename: string): Promise<string> => {
|
const getFileInput = async (
|
||||||
|
filename: string,
|
||||||
|
required: boolean = true
|
||||||
|
): Promise<string> => {
|
||||||
const validator = async (filePath: string) => {
|
const validator = async (filePath: string) => {
|
||||||
|
if (!required) return true
|
||||||
|
|
||||||
if (!filePath) return `Path to ${filename} is required.`
|
if (!filePath) return `Path to ${filename} is required.`
|
||||||
|
|
||||||
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
|
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getString } from '@sasjs/utils/input'
|
import { getString } from '@sasjs/utils/input'
|
||||||
import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
|
||||||
|
import { RunTimeType } from './verifyEnvVariables'
|
||||||
const isWindows = () => process.platform === 'win32'
|
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, NODE_PATH } = process.env
|
const { SAS_PATH, NODE_PATH } = process.env
|
||||||
|
|
||||||
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
let sasLoc, nodeLoc
|
||||||
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
|
||||||
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||||
|
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.runTimes.includes(RunTimeType.JS)) {
|
||||||
|
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||||
|
}
|
||||||
|
|
||||||
return { sasLoc, nodeLoc }
|
return { sasLoc, nodeLoc }
|
||||||
}
|
}
|
||||||
@@ -55,7 +60,7 @@ const getSASLocation = async (): Promise<string> => {
|
|||||||
: '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas'
|
: '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas'
|
||||||
|
|
||||||
const targetName = await getString(
|
const targetName = await getString(
|
||||||
'Please enter path to SAS executable (absolute path): ',
|
'Please enter full path to a SAS executable with UTF-8 encoding: ',
|
||||||
validator,
|
validator,
|
||||||
defaultLocation
|
defaultLocation
|
||||||
)
|
)
|
||||||
@@ -75,11 +80,11 @@ const getNodeLocation = async (): Promise<string> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultLocation = isWindows()
|
const defaultLocation = isWindows()
|
||||||
? 'C:\\Program Files\\nodejs\\'
|
? 'C:\\Program Files\\nodejs\\node.exe'
|
||||||
: '/usr/local/nodejs/bin'
|
: '/usr/local/nodejs/bin/node.sh'
|
||||||
|
|
||||||
const targetName = await getString(
|
const targetName = await getString(
|
||||||
'Please enter path to nodejs executable (absolute path): ',
|
'Please enter full path to a NodeJS executable: ',
|
||||||
validator,
|
validator,
|
||||||
defaultLocation
|
defaultLocation
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ import { RunTimeType } from '.'
|
|||||||
|
|
||||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||||
const ext = path.extname(programPath)
|
const ext = path.extname(programPath)
|
||||||
// if program path is provided with extension we should split that into code path and ext as run time
|
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
||||||
if (ext) {
|
// we should use that extension to determine the appropriate runTime
|
||||||
|
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
||||||
const runTime = ext.slice(1)
|
const runTime = ext.slice(1)
|
||||||
const runTimeTypes = Object.values(RunTimeType)
|
|
||||||
|
|
||||||
if (!runTimeTypes.includes(runTime as RunTimeType)) {
|
|
||||||
throw `The '${runTime}' runtime is not supported.`
|
|
||||||
}
|
|
||||||
|
|
||||||
const codePath = path
|
const codePath = path
|
||||||
.join(getFilesFolder(), programPath)
|
.join(getFilesFolder(), programPath)
|
||||||
|
|||||||
@@ -1,6 +1,73 @@
|
|||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
|
import Group from '../model/Group'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
|
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||||
|
|
||||||
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
|
export const SECRETS: ConfigurationType = {
|
||||||
|
ACCESS_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
REFRESH_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
AUTH_CODE_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
SESSION_SECRET: randomBytes(64).toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const seedDB = async (): Promise<ConfigurationType> => {
|
||||||
|
// Checking if client is already in the database
|
||||||
|
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||||
|
if (!clientExist) {
|
||||||
|
const client = new Client(CLIENT)
|
||||||
|
await client.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if 'AllUsers' Group is already in the database
|
||||||
|
let groupExist = await Group.findOne({ name: GROUP.name })
|
||||||
|
if (!groupExist) {
|
||||||
|
const group = new Group(GROUP)
|
||||||
|
groupExist = await group.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if user is already in the database
|
||||||
|
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
|
if (!usernameExist) {
|
||||||
|
const user = new User(ADMIN_USER)
|
||||||
|
usernameExist = await user.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupExist.hasUser(usernameExist)) {
|
||||||
|
groupExist.addUser(usernameExist)
|
||||||
|
console.log(
|
||||||
|
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checking if configuration is present in the database
|
||||||
|
let configExist = await Configuration.findOne()
|
||||||
|
if (!configExist) {
|
||||||
|
const configuration = new Configuration(SECRETS)
|
||||||
|
configExist = await configuration.save()
|
||||||
|
|
||||||
|
console.log('DB Seed - configuration added')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ACCESS_TOKEN_SECRET: configExist.ACCESS_TOKEN_SECRET,
|
||||||
|
REFRESH_TOKEN_SECRET: configExist.REFRESH_TOKEN_SECRET,
|
||||||
|
AUTH_CODE_SECRET: configExist.AUTH_CODE_SECRET,
|
||||||
|
SESSION_SECRET: configExist.SESSION_SECRET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUP = {
|
||||||
|
name: 'AllUsers',
|
||||||
|
description: 'Group contains all users'
|
||||||
|
}
|
||||||
const CLIENT = {
|
const CLIENT = {
|
||||||
clientId: 'clientID1',
|
clientId: 'clientID1',
|
||||||
clientSecret: 'clientSecret'
|
clientSecret: 'clientSecret'
|
||||||
@@ -13,23 +80,3 @@ const ADMIN_USER = {
|
|||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const seedDB = async () => {
|
|
||||||
// Checking if client is already in the database
|
|
||||||
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
|
||||||
if (!clientExist) {
|
|
||||||
const client = new Client(CLIENT)
|
|
||||||
await client.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checking if user is already in the database
|
|
||||||
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
|
||||||
if (!usernameExist) {
|
|
||||||
const user = new User(ADMIN_USER)
|
|
||||||
await user.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
import { getDesktopFields, ModeType, RunTimeType } from '.'
|
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
|
||||||
|
|
||||||
export const setProcessVariables = async () => {
|
export const setProcessVariables = async () => {
|
||||||
|
const { MODE, RUN_TIMES } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
|
// it should prevent connecting to the real database
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
const secrets = await connectDB()
|
||||||
|
|
||||||
|
process.secrets = secrets
|
||||||
|
} else {
|
||||||
|
process.secrets = SECRETS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { MODE } = process.env
|
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
process.sasLoc = process.env.SAS_PATH as string
|
process.sasLoc = process.env.SAS_PATH
|
||||||
process.nodeLoc = process.env.NODE_PATH as string
|
process.nodeLoc = process.env.NODE_PATH
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
const { sasLoc, nodeLoc } = await getDesktopFields()
|
||||||
|
|
||||||
@@ -26,9 +40,6 @@ export const setProcessVariables = async () => {
|
|||||||
await createFolder(absPath)
|
await createFolder(absPath)
|
||||||
process.driveLoc = getRealPath(absPath)
|
process.driveLoc = getRealPath(absPath)
|
||||||
|
|
||||||
const { RUN_TIMES } = process.env
|
|
||||||
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
|
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
console.log('runTimes: ', process.runTimes)
|
console.log('runTimes: ', process.runTimes)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
|||||||
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
code: Joi.string().required(),
|
code: Joi.string().required(),
|
||||||
runTime: Joi.string().valid(...Object.values(RunTimeType))
|
runTime: Joi.string().valid(...process.runTimes)
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -78,33 +78,7 @@ const verifyMODE = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Server) {
|
if (process.env.MODE === ModeType.Server) {
|
||||||
const {
|
const { DB_CONNECT } = process.env
|
||||||
ACCESS_TOKEN_SECRET,
|
|
||||||
REFRESH_TOKEN_SECRET,
|
|
||||||
AUTH_CODE_SECRET,
|
|
||||||
SESSION_SECRET,
|
|
||||||
DB_CONNECT
|
|
||||||
} = process.env
|
|
||||||
|
|
||||||
if (!ACCESS_TOKEN_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!REFRESH_TOKEN_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!AUTH_CODE_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!SESSION_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test')
|
if (process.env.NODE_ENV !== 'test')
|
||||||
if (!DB_CONNECT)
|
if (!DB_CONNECT)
|
||||||
@@ -129,16 +103,16 @@ const verifyPROTOCOL = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
||||||
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
const { PRIVATE_KEY, CERT_CHAIN } = process.env
|
||||||
|
|
||||||
if (!PRIVATE_KEY)
|
if (!PRIVATE_KEY)
|
||||||
errors.push(
|
errors.push(
|
||||||
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!FULL_CHAIN)
|
if (!CERT_CHAIN)
|
||||||
errors.push(
|
errors.push(
|
||||||
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
`- CERT_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,5 +232,5 @@ const DEFAULTS = {
|
|||||||
PORT: '5000',
|
PORT: '5000',
|
||||||
HELMET_COEP: HelmetCoepType.TRUE,
|
HELMET_COEP: HelmetCoepType.TRUE,
|
||||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||||
RUN_TIMES: `${RunTimeType.SAS},${RunTimeType.JS}`
|
RUN_TIMES: RunTimeType.SAS
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export const extractJSONFromZip = async (zipFile: Express.Multer.File) => {
|
|||||||
|
|
||||||
for await (const entry of zip) {
|
for await (const entry of zip) {
|
||||||
const fileName = entry.path as string
|
const fileName = entry.path as string
|
||||||
if (fileName.toUpperCase().endsWith('.JSON') && fileName === fileInZip) {
|
// grab the first json found in .zip
|
||||||
|
if (fileName.toUpperCase().endsWith('.JSON')) {
|
||||||
fileContent = await entry.buffer()
|
fileContent = await entry.buffer()
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -48,7 +48,16 @@ const Studio = () => {
|
|||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
const [webout, setWebout] = useState('')
|
const [webout, setWebout] = useState('')
|
||||||
const [tab, setTab] = useState('1')
|
const [tab, setTab] = useState('1')
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState(RunTimeType.SAS)
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
|
}, [appContext.runTimes])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||||
|
}, [runTimes])
|
||||||
|
|
||||||
const handleTabChange = (_e: any, newValue: string) => {
|
const handleTabChange = (_e: any, newValue: string) => {
|
||||||
setTab(newValue)
|
setTab(newValue)
|
||||||
@@ -153,7 +162,7 @@ const Studio = () => {
|
|||||||
</TabList>
|
</TabList>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TabPanel style={{ paddingBottom: 0 }} value="1">
|
<TabPanel sx={{ paddingBottom: 0 }} value="1">
|
||||||
<div className={classes.subMenu}>
|
<div className={classes.subMenu}>
|
||||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
<Tooltip title="CTRL+ENTER will also run SAS code">
|
||||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
||||||
@@ -174,8 +183,10 @@ const Studio = () => {
|
|||||||
value={selectedRunTime}
|
value={selectedRunTime}
|
||||||
onChange={handleChangeRunTime}
|
onChange={handleChangeRunTime}
|
||||||
>
|
>
|
||||||
{appContext.runTimes.map((runTime) => (
|
{runTimes.map((runTime) => (
|
||||||
<MenuItem value={runTime}>{runTime}</MenuItem>
|
<MenuItem key={runTime} value={runTime}>
|
||||||
|
{runTime}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
axios.get('/logout').then(() => {
|
axios.get('/SASLogon/logout').then(() => {
|
||||||
setLoggedIn(false)
|
setLoggedIn(false)
|
||||||
setUsername('')
|
setUsername('')
|
||||||
setDisplayName('')
|
setDisplayName('')
|
||||||
|
|||||||
Reference in New Issue
Block a user