mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b243e62ece | ||
|
|
88c3056e97 | ||
| 203303b659 | |||
| 835709bd36 | |||
| 69f2576ee6 | |||
|
|
305077f36e | ||
|
|
96eca3a35d | ||
|
|
0f5c815c25 | ||
|
|
acccef1e99 | ||
| abc34ea047 | |||
| 71c429b093 | |||
|
|
c126f2d5d9 | ||
|
|
34dd95d16e | ||
| 1192583843 | |||
|
|
518815acf1 | ||
|
|
80b7e14ed5 | ||
| 23c997b3be | |||
| 39ba995355 | |||
|
|
0e081e024b | ||
|
|
6a84bd0387 | ||
| 98d177a691 | |||
| 4dcee4b3c3 | |||
|
|
4ffc1ec6a9 | ||
|
|
5a1d168e83 | ||
|
|
515c976685 | ||
| 112431a1b7 | |||
| c26485afec | |||
| 1d48f8856b | |||
| 68758aa616 | |||
| 8b8c43c21b | |||
| 4581f32534 | |||
| b47e74a7e1 | |||
| b27d684145 | |||
|
|
6b666d5554 | ||
|
|
b5f0911858 | ||
| b86ba5b8a3 | |||
| 200f6c596a | |||
|
|
1b7ccda6e9 | ||
|
|
532035d835 | ||
|
|
7ae862c5ce | ||
|
|
ab5858b8af | ||
|
|
a39f5dd9f1 | ||
|
|
3ea444756c | ||
|
|
96399ecbbe | ||
| bb054938c5 | |||
|
|
fb6a556630 | ||
|
|
9dbd8e16bd | ||
| fe07c41f5f | |||
| acc25cbd68 | |||
| 4ca61feda6 | |||
| abd5c64b4a | |||
| 2413c05fea | |||
|
|
4c874c2c39 | ||
|
|
d819d79bc9 | ||
| c51b50428f | |||
|
|
e10a0554f0 | ||
|
|
337e2eb2a0 | ||
| 66f8e7840b | |||
| 1c9d167f86 | |||
|
|
7e684b54a6 | ||
|
|
aafda2922b | ||
| 418bf41e38 | |||
| 81f0b03b09 | |||
| fe5ae44aab | |||
| 36be3a7d5e | |||
| 6434123401 | |||
|
|
0a6b972c65 | ||
|
|
be11707042 | ||
| 2412622367 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,8 +5,6 @@ node_modules/
|
|||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
sasjs_root/
|
sasjs_root/
|
||||||
api/mocks/custom/*
|
|
||||||
!api/mocks/custom/.keep
|
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
|||||||
120
CHANGELOG.md
120
CHANGELOG.md
@@ -1,3 +1,123 @@
|
|||||||
|
## [0.28.6](https://github.com/sasjs/server/compare/v0.28.5...v0.28.6) (2023-01-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* show loading spinner on login screen while request is in process ([69f2576](https://github.com/sasjs/server/commit/69f2576ee6d3d7b7f3325922a88656d511e3ac88))
|
||||||
|
|
||||||
|
## [0.28.5](https://github.com/sasjs/server/compare/v0.28.4...v0.28.5) (2023-01-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding NOPRNGETLIST system option for faster startup ([96eca3a](https://github.com/sasjs/server/commit/96eca3a35dce4521150257ee019beb4488c8a08f))
|
||||||
|
|
||||||
|
## [0.28.4](https://github.com/sasjs/server/compare/v0.28.3...v0.28.4) (2022-12-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* replace main class with container class ([71c429b](https://github.com/sasjs/server/commit/71c429b093b91e2444ae75d946579dccc2e48636))
|
||||||
|
|
||||||
|
## [0.28.3](https://github.com/sasjs/server/compare/v0.28.2...v0.28.3) (2022-12-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* stringify json file ([1192583](https://github.com/sasjs/server/commit/1192583843d7efd1a6ab6943207f394c3ae966be))
|
||||||
|
|
||||||
|
## [0.28.2](https://github.com/sasjs/server/compare/v0.28.1...v0.28.2) (2022-12-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* execute child process asyncronously ([23c997b](https://github.com/sasjs/server/commit/23c997b3beabeb6b733ae893031d2f1a48f28ad2))
|
||||||
|
* JS / Python / R session folders should be NEW folders, not existing SAS folders ([39ba995](https://github.com/sasjs/server/commit/39ba995355daa24bb7ab22720f8fc57d2dc85f40))
|
||||||
|
|
||||||
|
## [0.28.1](https://github.com/sasjs/server/compare/v0.28.0...v0.28.1) (2022-11-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update the content type header after the program has been executed ([4dcee4b](https://github.com/sasjs/server/commit/4dcee4b3c3950d402220b8f451c50ad98a317d83))
|
||||||
|
|
||||||
|
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update the response header of request to stp/execute routes ([112431a](https://github.com/sasjs/server/commit/112431a1b7461989c04100418d67d975a2a8f354))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** add the api endpoint for updating user password ([4581f32](https://github.com/sasjs/server/commit/4581f325344eb68c5df5a28492f132312f15bb5c))
|
||||||
|
* ask for updated password on first login ([1d48f88](https://github.com/sasjs/server/commit/1d48f8856b1fbbf3ef868914558333190e04981f))
|
||||||
|
* **web:** add the UI for updating user password ([8b8c43c](https://github.com/sasjs/server/commit/8b8c43c21bde5379825c5ec44ecd81a92425f605))
|
||||||
|
|
||||||
|
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* on startup add webout.sas file in sasautos folder ([200f6c5](https://github.com/sasjs/server/commit/200f6c596a6e732d799ed408f1f0fd92f216ba58))
|
||||||
|
|
||||||
|
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* comments ([7ae862c](https://github.com/sasjs/server/commit/7ae862c5ce720e9483d4728f4295dede4f849436))
|
||||||
|
|
||||||
|
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* change the expiration of access/refresh tokens from days to seconds ([bb05493](https://github.com/sasjs/server/commit/bb054938c5bd0535ae6b9da93ba0b14f9b80ddcd))
|
||||||
|
|
||||||
|
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* make access token duration configurable when creating client/secret ([2413c05](https://github.com/sasjs/server/commit/2413c05fea3960f7e5c3c8b7b2f85d61314f08db))
|
||||||
|
* make refresh token duration configurable ([abd5c64](https://github.com/sasjs/server/commit/abd5c64b4a726e3f17594a98111b6aa269b71fee))
|
||||||
|
|
||||||
|
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
|
||||||
|
|
||||||
|
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
|
||||||
|
|
||||||
|
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
|
||||||
|
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
|
||||||
|
|
||||||
|
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
|
||||||
|
|
||||||
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
|
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ R_PATH=/usr/bin/Rscript
|
|||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
|
||||||
|
# This location is for files, sasjs packages and appStreamConfig.json
|
||||||
|
DRIVE_LOCATION=./sasjs_root/drive
|
||||||
|
|
||||||
|
|
||||||
# options: [http|https] default: http
|
# options: [http|https] default: http
|
||||||
PROTOCOL=
|
PROTOCOL=
|
||||||
|
|
||||||
@@ -103,6 +107,11 @@ PORT=
|
|||||||
# If not present, mocking function is disabled
|
# If not present, mocking function is disabled
|
||||||
MOCK_SERVERTYPE=
|
MOCK_SERVERTYPE=
|
||||||
|
|
||||||
|
# default: /api/mocks
|
||||||
|
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
|
||||||
|
# Server will automatically use subdirectory accordingly
|
||||||
|
STATIC_MOCK_LOCATION=
|
||||||
|
|
||||||
#
|
#
|
||||||
## Additional SAS Options
|
## Additional SAS Options
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ PYTHON_PATH=/usr/bin/python
|
|||||||
R_PATH=/usr/bin/Rscript
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
DRIVE_LOCATION=./sasjs_root/drive
|
||||||
|
|
||||||
LOG_FORMAT_MORGAN=common
|
LOG_FORMAT_MORGAN=common
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
||||||
<!--form container-->
|
<!--form container-->
|
||||||
<input type="hidden" name="lt" value="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" aria-hidden="true" />
|
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
|
||||||
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
||||||
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
||||||
|
|
||||||
3199
api/package-lock.json
generated
3199
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@
|
|||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"http-headers-validation": "^0.0.1",
|
"http-headers-validation": "^0.0.1",
|
||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "^8.0.0",
|
"mongodb-memory-server": "8.11.4",
|
||||||
"nodejs-file-downloader": "4.10.2",
|
"nodejs-file-downloader": "4.10.2",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pkg": "5.6.0",
|
"pkg": "5.6.0",
|
||||||
|
|||||||
@@ -47,6 +47,21 @@ components:
|
|||||||
- userId
|
- userId
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
UpdatePasswordPayload:
|
||||||
|
properties:
|
||||||
|
currentPassword:
|
||||||
|
type: string
|
||||||
|
description: 'Current Password'
|
||||||
|
example: currentPasswordString
|
||||||
|
newPassword:
|
||||||
|
type: string
|
||||||
|
description: 'New Password'
|
||||||
|
example: newPassword
|
||||||
|
required:
|
||||||
|
- currentPassword
|
||||||
|
- newPassword
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ClientPayload:
|
ClientPayload:
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
@@ -57,6 +72,16 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: 'Client Secret'
|
description: 'Client Secret'
|
||||||
example: someRandomCryptoString
|
example: someRandomCryptoString
|
||||||
|
accessTokenExpiration:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: 'Number of seconds after which access token will expire. Default is 86400 (1 day)'
|
||||||
|
example: 86400
|
||||||
|
refreshTokenExpiration:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: 'Number of seconds after which access token will expire. Default is 2592000 (30 days)'
|
||||||
|
example: 2592000
|
||||||
required:
|
required:
|
||||||
- clientId
|
- clientId
|
||||||
- clientSecret
|
- clientSecret
|
||||||
@@ -509,6 +534,27 @@ components:
|
|||||||
- setting
|
- setting
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
SessionResponse:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
displayName:
|
||||||
|
type: string
|
||||||
|
isAdmin:
|
||||||
|
type: boolean
|
||||||
|
needsToUpdatePassword:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- username
|
||||||
|
- displayName
|
||||||
|
- isAdmin
|
||||||
|
- needsToUpdatePassword
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ExecutePostRequestPayload:
|
ExecutePostRequestPayload:
|
||||||
properties:
|
properties:
|
||||||
_program:
|
_program:
|
||||||
@@ -622,6 +668,25 @@ paths:
|
|||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters: []
|
||||||
|
/SASjsApi/auth/updatePassword:
|
||||||
|
patch:
|
||||||
|
operationId: UpdatePassword
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 'No content'
|
||||||
|
summary: 'Update user''s password.'
|
||||||
|
tags:
|
||||||
|
- Auth
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UpdatePasswordPayload'
|
||||||
/SASjsApi/authConfig:
|
/SASjsApi/authConfig:
|
||||||
get:
|
get:
|
||||||
operationId: GetDetail
|
operationId: GetDetail
|
||||||
@@ -679,8 +744,8 @@ paths:
|
|||||||
$ref: '#/components/schemas/ClientPayload'
|
$ref: '#/components/schemas/ClientPayload'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
|
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}
|
||||||
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
|
summary: "Admin only task. Create client with the following attributes:\nClientId,\nClientSecret,\naccessTokenExpiration (optional),\nrefreshTokenExpiration (optional)"
|
||||||
tags:
|
tags:
|
||||||
- Client
|
- Client
|
||||||
security:
|
security:
|
||||||
@@ -1680,7 +1745,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserResponse'
|
$ref: '#/components/schemas/SessionResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
||||||
@@ -1777,7 +1842,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
properties:
|
properties:
|
||||||
user: {properties: {isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [isAdmin, displayName, username, id], type: object}
|
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
|
||||||
loggedIn: {type: boolean}
|
loggedIn: {type: boolean}
|
||||||
required:
|
required:
|
||||||
- user
|
- user
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const configureCors = (app: Express) => {
|
|||||||
whiteList.push(url.replace(/\/$/, ''))
|
whiteList.push(url.replace(/\/$/, ''))
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('All CORS Requests are enabled for:', whiteList)
|
process.logger.info('All CORS Requests are enabled for:', whiteList)
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const configureLogger = (app: Express) => {
|
|||||||
path: logsFolder
|
path: logsFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Writing Logs to :', path.join(logsFolder, filename))
|
process.logger.info('Writing Logs to :', path.join(logsFolder, filename))
|
||||||
|
|
||||||
options = { stream: accessLogStream }
|
options = { stream: accessLogStream }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import dotenv from 'dotenv'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
|
createWeboutSasFile,
|
||||||
|
getFilesFolder,
|
||||||
|
getPackagesFolder,
|
||||||
getWebBuildFolder,
|
getWebBuildFolder,
|
||||||
instantiateLogger,
|
instantiateLogger,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
ReturnCode,
|
ReturnCode,
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders,
|
setupFilesFolder,
|
||||||
|
setupPackagesFolder,
|
||||||
|
setupUserAutoExec,
|
||||||
verifyEnvVariables
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +24,7 @@ import {
|
|||||||
configureLogger,
|
configureLogger,
|
||||||
configureSecurity
|
configureSecurity
|
||||||
} from './app-modules'
|
} from './app-modules'
|
||||||
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
@@ -29,7 +35,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
|||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
console.error(err.stack)
|
process.logger.error(err.stack)
|
||||||
res.status(500).send('Something broke!')
|
res.status(500).send('Something broke!')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +68,21 @@ export default setProcessVariables().then(async () => {
|
|||||||
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||||
app.use(express.urlencoded({ extended: true }))
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
await setupFolders()
|
await setupUserAutoExec()
|
||||||
await copySASjsCore()
|
|
||||||
|
if (!(await folderExists(getFilesFolder()))) await setupFilesFolder()
|
||||||
|
|
||||||
|
if (!(await folderExists(getPackagesFolder()))) await setupPackagesFolder()
|
||||||
|
|
||||||
|
const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos')
|
||||||
|
if (await folderExists(sasautosPath)) {
|
||||||
|
process.logger.warn(
|
||||||
|
`SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await copySASjsCore()
|
||||||
|
await createWeboutSasFile()
|
||||||
|
}
|
||||||
|
|
||||||
// loading these modules after setting up variables due to
|
// loading these modules after setting up variables due to
|
||||||
// multer's usage of process var process.driveLoc
|
// multer's usage of process var process.driveLoc
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
import express from 'express'
|
||||||
|
import {
|
||||||
|
Security,
|
||||||
|
Route,
|
||||||
|
Tags,
|
||||||
|
Example,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Request,
|
||||||
|
Body,
|
||||||
|
Query,
|
||||||
|
Hidden
|
||||||
|
} from 'tsoa'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +20,8 @@ import {
|
|||||||
removeTokensInDB,
|
removeTokensInDB,
|
||||||
saveTokensInDB
|
saveTokensInDB
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import Client from '../model/Client'
|
||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
@Route('SASjsApi/auth')
|
@Route('SASjsApi/auth')
|
||||||
@Tags('Auth')
|
@Tags('Auth')
|
||||||
@@ -61,6 +75,18 @@ export class AuthController {
|
|||||||
public async logout(@Query() @Hidden() data?: InfoJWT) {
|
public async logout(@Query() @Hidden() data?: InfoJWT) {
|
||||||
return logout(data!)
|
return logout(data!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Update user's password.
|
||||||
|
*/
|
||||||
|
@Security('bearerAuth')
|
||||||
|
@Patch('updatePassword')
|
||||||
|
public async updatePassword(
|
||||||
|
@Request() req: express.Request,
|
||||||
|
@Body() body: UpdatePasswordPayload
|
||||||
|
) {
|
||||||
|
return updatePassword(req, body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = async (data: any): Promise<TokenResponse> => {
|
const token = async (data: any): Promise<TokenResponse> => {
|
||||||
@@ -83,8 +109,17 @@ const token = async (data: any): Promise<TokenResponse> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = generateAccessToken(userInfo)
|
const client = await Client.findOne({ clientId })
|
||||||
const refreshToken = generateRefreshToken(userInfo)
|
if (!client) throw new Error('Invalid clientId.')
|
||||||
|
|
||||||
|
const accessToken = generateAccessToken(
|
||||||
|
userInfo,
|
||||||
|
client.accessTokenExpiration
|
||||||
|
)
|
||||||
|
const refreshToken = generateRefreshToken(
|
||||||
|
userInfo,
|
||||||
|
client.refreshTokenExpiration
|
||||||
|
)
|
||||||
|
|
||||||
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
||||||
|
|
||||||
@@ -92,8 +127,17 @@ const token = async (data: any): Promise<TokenResponse> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
||||||
const accessToken = generateAccessToken(userInfo)
|
const client = await Client.findOne({ clientId: userInfo.clientId })
|
||||||
const refreshToken = generateRefreshToken(userInfo)
|
if (!client) throw new Error('Invalid clientId.')
|
||||||
|
|
||||||
|
const accessToken = generateAccessToken(
|
||||||
|
userInfo,
|
||||||
|
client.accessTokenExpiration
|
||||||
|
)
|
||||||
|
const refreshToken = generateRefreshToken(
|
||||||
|
userInfo,
|
||||||
|
client.refreshTokenExpiration
|
||||||
|
)
|
||||||
|
|
||||||
await saveTokensInDB(
|
await saveTokensInDB(
|
||||||
userInfo.userId,
|
userInfo.userId,
|
||||||
@@ -109,6 +153,40 @@ const logout = async (userInfo: InfoJWT) => {
|
|||||||
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePassword = async (
|
||||||
|
req: express.Request,
|
||||||
|
data: UpdatePasswordPayload
|
||||||
|
) => {
|
||||||
|
const { currentPassword, newPassword } = data
|
||||||
|
const userId = req.user?.userId
|
||||||
|
const dbUser = await User.findOne({ userId })
|
||||||
|
|
||||||
|
if (!dbUser)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
message: `User not found!`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbUser?.authProvider) {
|
||||||
|
throw {
|
||||||
|
code: 405,
|
||||||
|
message:
|
||||||
|
'Can not update password of user that is created by an external auth provider.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validPass = dbUser.comparePassword(currentPassword)
|
||||||
|
if (!validPass)
|
||||||
|
throw {
|
||||||
|
code: 403,
|
||||||
|
message: `Invalid current password!`
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser.password = User.hashPassword(newPassword)
|
||||||
|
dbUser.needsToUpdatePassword = false
|
||||||
|
await dbUser.save()
|
||||||
|
}
|
||||||
|
|
||||||
interface TokenPayload {
|
interface TokenPayload {
|
||||||
/**
|
/**
|
||||||
* Client ID
|
* Client ID
|
||||||
@@ -135,6 +213,19 @@ interface TokenResponse {
|
|||||||
refreshToken: string
|
refreshToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdatePasswordPayload {
|
||||||
|
/**
|
||||||
|
* Current Password
|
||||||
|
* @example "currentPasswordString"
|
||||||
|
*/
|
||||||
|
currentPassword: string
|
||||||
|
/**
|
||||||
|
* New Password
|
||||||
|
* @example "newPassword"
|
||||||
|
*/
|
||||||
|
newPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
const verifyAuthCode = async (
|
const verifyAuthCode = async (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
code: string
|
code: string
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ const synchroniseWithLDAP = async () => {
|
|||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
password: hashPassword,
|
password: hashPassword,
|
||||||
authProvider: AuthProviderType.LDAP
|
authProvider: AuthProviderType.LDAP,
|
||||||
|
needsToUpdatePassword: false
|
||||||
})
|
})
|
||||||
|
|
||||||
importedUsers.push(user)
|
importedUsers.push(user)
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
|
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
|
||||||
|
|
||||||
import Client, { ClientPayload } from '../model/Client'
|
import Client, {
|
||||||
|
ClientPayload,
|
||||||
|
NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
|
} from '../model/Client'
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/client')
|
@Route('SASjsApi/client')
|
||||||
@Tags('Client')
|
@Tags('Client')
|
||||||
export class ClientController {
|
export class ClientController {
|
||||||
/**
|
/**
|
||||||
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
|
* @summary Admin only task. Create client with the following attributes:
|
||||||
|
* ClientId,
|
||||||
|
* ClientSecret,
|
||||||
|
* accessTokenExpiration (optional),
|
||||||
|
* refreshTokenExpiration (optional)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<ClientPayload>({
|
@Example<ClientPayload>({
|
||||||
clientId: 'someFormattedClientID1234',
|
clientId: 'someFormattedClientID1234',
|
||||||
clientSecret: 'someRandomCryptoString'
|
clientSecret: 'someRandomCryptoString',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
})
|
})
|
||||||
@Post('/')
|
@Post('/')
|
||||||
public async createClient(
|
public async createClient(
|
||||||
@@ -22,8 +31,13 @@ export class ClientController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createClient = async (data: any): Promise<ClientPayload> => {
|
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
||||||
const { clientId, clientSecret } = data
|
const {
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
accessTokenExpiration,
|
||||||
|
refreshTokenExpiration
|
||||||
|
} = data
|
||||||
|
|
||||||
// Checking if client is already in the database
|
// Checking if client is already in the database
|
||||||
const clientExist = await Client.findOne({ clientId })
|
const clientExist = await Client.findOne({ clientId })
|
||||||
@@ -32,13 +46,17 @@ const createClient = async (data: any): Promise<ClientPayload> => {
|
|||||||
// Create a new client
|
// Create a new client
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
accessTokenExpiration,
|
||||||
|
refreshTokenExpiration
|
||||||
})
|
})
|
||||||
|
|
||||||
const savedClient = await client.save()
|
const savedClient = await client.save()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clientId: savedClient.clientId,
|
clientId: savedClient.clientId,
|
||||||
clientSecret: savedClient.clientSecret
|
clientSecret: savedClient.clientSecret,
|
||||||
|
accessTokenExpiration: savedClient.accessTokenExpiration,
|
||||||
|
refreshTokenExpiration: savedClient.refreshTokenExpiration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface ExecuteFileParams {
|
|||||||
returnJson?: boolean
|
returnJson?: boolean
|
||||||
session?: Session
|
session?: Session
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
|
forceStringResult?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||||
@@ -42,7 +43,8 @@ export class ExecutionController {
|
|||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
returnJson,
|
||||||
session,
|
session,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
}: ExecuteFileParams) {
|
}: ExecuteFileParams) {
|
||||||
const program = await readFile(programPath)
|
const program = await readFile(programPath)
|
||||||
|
|
||||||
@@ -53,7 +55,8 @@ export class ExecutionController {
|
|||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
returnJson,
|
||||||
session,
|
session,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +66,8 @@ export class ExecutionController {
|
|||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController = getSessionController(runTime)
|
||||||
|
|
||||||
@@ -74,6 +78,7 @@ export class ExecutionController {
|
|||||||
|
|
||||||
const logPath = path.join(session.path, 'log.log')
|
const logPath = path.join(session.path, 'log.log')
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
|
|
||||||
const weboutPath = path.join(session.path, 'webout.txt')
|
const weboutPath = path.join(session.path, 'webout.txt')
|
||||||
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||||
|
|
||||||
@@ -101,10 +106,15 @@ export class ExecutionController {
|
|||||||
? await readFile(headersPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
|
|
||||||
|
if (isDebugOn(vars)) {
|
||||||
|
httpHeaders['content-type'] = 'text/plain'
|
||||||
|
}
|
||||||
|
|
||||||
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse && !forceStringResult
|
||||||
? await readFileBinary(weboutPath)
|
? await readFileBinary(weboutPath)
|
||||||
: await readFile(weboutPath)
|
: await readFile(weboutPath)
|
||||||
: ''
|
: ''
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
await createFile(headersPath, 'Content-type: text/plain')
|
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
|
||||||
|
|
||||||
this.sessions.push(session)
|
this.sessions.push(session)
|
||||||
return session
|
return session
|
||||||
@@ -94,7 +94,7 @@ export class SASSessionController extends SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
await createFile(headersPath, 'Content-type: text/plain')
|
await createFile(headersPath, 'content-type: text/html; charset=utf-8\n')
|
||||||
|
|
||||||
// we do not want to leave sessions running forever
|
// we do not want to leave sessions running forever
|
||||||
// we clean them up after a predefined period, if unused
|
// we clean them up after a predefined period, if unused
|
||||||
@@ -140,17 +140,18 @@ ${autoExecContent}`
|
|||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-NOPRNGETLIST' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
console.log('session completed', session)
|
process.logger.info('session completed', session)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
session.crashed = err.toString()
|
session.crashed = err.toString()
|
||||||
console.log('session crashed', session.id, session.crashed)
|
process.logger.error('session crashed', session.id, session.crashed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// we have a triggered session - add to array
|
// we have a triggered session - add to array
|
||||||
@@ -170,7 +171,10 @@ ${autoExecContent}`
|
|||||||
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
||||||
|
|
||||||
if (session.crashed)
|
if (session.crashed)
|
||||||
console.log('session crashed! while waiting to be ready', session.crashed)
|
process.logger.error(
|
||||||
|
'session crashed! while waiting to be ready',
|
||||||
|
session.crashed
|
||||||
|
)
|
||||||
|
|
||||||
session.ready = true
|
session.ready = true
|
||||||
}
|
}
|
||||||
@@ -203,12 +207,15 @@ ${autoExecContent}`
|
|||||||
export const getSessionController = (
|
export const getSessionController = (
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
): SessionController => {
|
): SessionController => {
|
||||||
if (process.sessionController) return process.sessionController
|
if (runTime === RunTimeType.SAS) {
|
||||||
|
process.sasSessionController =
|
||||||
|
process.sasSessionController || new SASSessionController()
|
||||||
|
|
||||||
|
return process.sasSessionController
|
||||||
|
}
|
||||||
|
|
||||||
process.sessionController =
|
process.sessionController =
|
||||||
runTime === RunTimeType.SAS
|
process.sessionController || new SessionController()
|
||||||
? new SASSessionController()
|
|
||||||
: new SessionController()
|
|
||||||
|
|
||||||
return process.sessionController
|
return process.sessionController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import { WriteStream, createWriteStream } from 'fs'
|
||||||
import { execFileSync } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { once } from 'stream'
|
import { once } from 'stream'
|
||||||
import { createFile, moveFile } from '@sasjs/utils'
|
import { createFile, moveFile } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session } from '../../types'
|
import { PreProgramVars, Session } from '../../types'
|
||||||
@@ -105,30 +105,58 @@ export const processProgram = async (
|
|||||||
throw new Error('Invalid runtime!')
|
throw new Error('Invalid runtime!')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await createFile(codePath, program)
|
||||||
await createFile(codePath, program)
|
|
||||||
|
|
||||||
// create a stream that will write to console outputs to log file
|
// create a stream that will write to console outputs to log file
|
||||||
const writeStream = fs.createWriteStream(logPath)
|
const writeStream = createWriteStream(logPath)
|
||||||
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
// waiting for the open event so that we can have underlying file descriptor
|
await execFilePromise(executablePath, [codePath], writeStream)
|
||||||
await once(writeStream, 'open')
|
.then(() => {
|
||||||
|
session.completed = true
|
||||||
execFileSync(executablePath, [codePath], {
|
process.logger.info('session completed', session)
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
session.completed = true
|
||||||
|
session.crashed = err.toString()
|
||||||
|
process.logger.error('session crashed', session.id, session.crashed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// copy the code file to log and end write stream
|
// copy the code file to log and end write stream
|
||||||
writeStream.end(program)
|
writeStream.end(program)
|
||||||
|
|
||||||
session.completed = true
|
|
||||||
console.log('session completed', session)
|
|
||||||
} catch (err: any) {
|
|
||||||
session.completed = true
|
|
||||||
session.crashed = err.toString()
|
|
||||||
console.log('session crashed', session.id, session.crashed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisified child_process.execFile
|
||||||
|
*
|
||||||
|
* @param file - The name or path of the executable file to run.
|
||||||
|
* @param args - List of string arguments.
|
||||||
|
* @param writeStream - Child process stdout and stderr will be piped to it.
|
||||||
|
*
|
||||||
|
* @returns {Promise<{ stdout: string, stderr: string }>}
|
||||||
|
*/
|
||||||
|
const execFilePromise = (
|
||||||
|
file: string,
|
||||||
|
args: string[],
|
||||||
|
writeStream: WriteStream
|
||||||
|
): Promise<{ stdout: string; stderr: string }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = execFile(file, args, (err, stdout, stderr) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
|
||||||
|
resolve({ stdout, stderr })
|
||||||
|
})
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
writeStream.write(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
writeStream.write(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ import { readFile } from '@sasjs/utils'
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Request, Post, Get } from 'tsoa'
|
import { Request, Post, Get } from 'tsoa'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { ExecutionController } from './internal'
|
||||||
|
import {
|
||||||
|
getPreProgramVariables,
|
||||||
|
getRunTimeAndFilePath,
|
||||||
|
makeFilesNamesMap
|
||||||
|
} from '../utils'
|
||||||
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
export interface Sas9Response {
|
export interface Sas9Response {
|
||||||
content: string
|
content: string
|
||||||
@@ -16,9 +26,17 @@ export interface MockFileRead {
|
|||||||
|
|
||||||
export class MockSas9Controller {
|
export class MockSas9Controller {
|
||||||
private loggedIn: string | undefined
|
private loggedIn: string | undefined
|
||||||
|
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||||
|
|
||||||
@Get('/SASStoredProcess')
|
@Get('/SASStoredProcess')
|
||||||
public async sasStoredProcess(): Promise<Sas9Response> {
|
public async sasStoredProcess(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
const username = req.query._username?.toString() || undefined
|
||||||
|
const password = req.query._password?.toString() || undefined
|
||||||
|
|
||||||
|
if (username && password) this.loggedIn = req.body.username
|
||||||
|
|
||||||
if (!this.loggedIn) {
|
if (!this.loggedIn) {
|
||||||
return {
|
return {
|
||||||
content: '',
|
content: '',
|
||||||
@@ -26,17 +44,87 @@ export class MockSas9Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let program = req.query._program?.toString() || undefined
|
||||||
|
const filePath: string[] = program
|
||||||
|
? program.replace('/', '').split('/')
|
||||||
|
: ['generic', 'sas-stored-process']
|
||||||
|
|
||||||
|
if (program) {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
this.mocksPath,
|
||||||
|
'sas9',
|
||||||
|
...filePath
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
'sas-stored-process'
|
...filePath
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess/do')
|
||||||
|
public async sasStoredProcessDoGet(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
const username = req.query._username?.toString() || undefined
|
||||||
|
const password = req.query._password?.toString() || undefined
|
||||||
|
|
||||||
|
if (username && password) this.loggedIn = username
|
||||||
|
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = req.query._program ?? req.body?._program
|
||||||
|
const filePath: string[] = ['generic', 'sas-stored-process']
|
||||||
|
|
||||||
|
if (program) {
|
||||||
|
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
|
||||||
|
const otherArgs = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { codePath, runTime } = await getRunTimeAndFilePath(
|
||||||
|
program + '.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await new ExecutionController().executeFile({
|
||||||
|
programPath: codePath,
|
||||||
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
|
vars: vars,
|
||||||
|
otherArgs: otherArgs,
|
||||||
|
runTime,
|
||||||
|
forceStringResult: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: result.result as string
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
process.logger.error('err', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: 'No webout returned.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
...filePath
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/SASStoredProcess/do/')
|
@Post('/SASStoredProcess/do/')
|
||||||
public async sasStoredProcessDo(
|
public async sasStoredProcessDoPost(
|
||||||
@Request() req: express.Request
|
@Request() req: express.Request
|
||||||
): Promise<Sas9Response> {
|
): Promise<Sas9Response> {
|
||||||
if (!this.loggedIn) {
|
if (!this.loggedIn) {
|
||||||
@@ -53,23 +141,38 @@ export class MockSas9Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let program = req.query._program?.toString() || ''
|
const program = req.query._program ?? req.body?._program
|
||||||
program = program.replace('/', '')
|
const vars = {
|
||||||
|
...req.query,
|
||||||
|
...req.body,
|
||||||
|
_requestMethod: req.method,
|
||||||
|
_driveLoc: process.driveLoc
|
||||||
|
}
|
||||||
|
const filesNamesMap = req.files?.length
|
||||||
|
? makeFilesNamesMap(req.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||||
|
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
|
||||||
|
try {
|
||||||
|
const result = await new ExecutionController().executeFile({
|
||||||
|
programPath: codePath,
|
||||||
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
|
vars: vars,
|
||||||
|
otherArgs: otherArgs,
|
||||||
|
runTime,
|
||||||
|
session: req.sasjsSession,
|
||||||
|
forceStringResult: true
|
||||||
|
})
|
||||||
|
|
||||||
const content = await getMockResponseFromFile([
|
return {
|
||||||
process.cwd(),
|
content: result.result as string
|
||||||
'mocks',
|
}
|
||||||
...program.split('/')
|
} catch (err) {
|
||||||
])
|
process.logger.error('err', err)
|
||||||
|
|
||||||
if (content.error) {
|
|
||||||
return content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = parseJsonIfValid(content.content)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: parsedContent
|
content: 'No webout returned.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +188,8 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'logged-in'
|
'logged-in'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -95,21 +198,27 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'login'
|
'login'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/SASLogon/login')
|
@Post('/SASLogon/login')
|
||||||
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||||
|
if (req.body.lt && req.body.lt !== 'validtoken')
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
|
||||||
this.loggedIn = req.body.username
|
this.loggedIn = req.body.username
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'logged-in'
|
'logged-in'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -122,8 +231,8 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'public-access-denied'
|
'public-access-denied'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -131,8 +240,8 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'logged-out'
|
'logged-out'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -152,23 +261,6 @@ export class MockSas9Controller {
|
|||||||
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If JSON is valid it will be parsed otherwise will return text unaltered
|
|
||||||
* @param content string to be parsed
|
|
||||||
* @returns JSON or string
|
|
||||||
*/
|
|
||||||
const parseJsonIfValid = (content: string) => {
|
|
||||||
let fileContent = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
fileContent = JSON.parse(content)
|
|
||||||
} catch (err: any) {
|
|
||||||
fileContent = content
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileContent
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMockResponseFromFile = async (
|
const getMockResponseFromFile = async (
|
||||||
filePath: string[]
|
filePath: string[]
|
||||||
): Promise<MockFileRead> => {
|
): Promise<MockFileRead> => {
|
||||||
@@ -177,7 +269,7 @@ const getMockResponseFromFile = async (
|
|||||||
|
|
||||||
let file = await readFile(filePathParsed).catch((err: any) => {
|
let file = await readFile(filePathParsed).catch((err: any) => {
|
||||||
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
|
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
|
||||||
console.error(errMsg)
|
process.logger.error(errMsg)
|
||||||
|
|
||||||
error = true
|
error = true
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import express from 'express'
|
|||||||
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
|
|
||||||
|
interface SessionResponse extends UserResponse {
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/session')
|
@Route('SASjsApi/session')
|
||||||
@Tags('Session')
|
@Tags('Session')
|
||||||
@@ -19,7 +23,7 @@ export class SessionController {
|
|||||||
@Get('/')
|
@Get('/')
|
||||||
public async session(
|
public async session(
|
||||||
@Request() request: express.Request
|
@Request() request: express.Request
|
||||||
): Promise<UserResponse> {
|
): Promise<SessionResponse> {
|
||||||
return session(request)
|
return session(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,5 +32,6 @@ const session = (req: express.Request) => ({
|
|||||||
id: req.user!.userId,
|
id: req.user!.userId,
|
||||||
username: req.user!.username,
|
username: req.user!.username,
|
||||||
displayName: req.user!.displayName,
|
displayName: req.user!.displayName,
|
||||||
isAdmin: req.user!.isAdmin
|
isAdmin: req.user!.isAdmin,
|
||||||
|
needsToUpdatePassword: req.user!.needsToUpdatePassword
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ const execute = async (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
req.res?.header(httpHeaders)
|
||||||
|
|
||||||
if (result instanceof Buffer) {
|
if (result instanceof Buffer) {
|
||||||
;(req as any).sasHeaders = httpHeaders
|
;(req as any).sasHeaders = httpHeaders
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ const login = async (
|
|||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
autoExec: user.autoExec
|
autoExec: user.autoExec,
|
||||||
|
needsToUpdatePassword: user.needsToUpdatePassword
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -113,7 +114,8 @@ const login = async (
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin
|
isAdmin: user.isAdmin,
|
||||||
|
needsToUpdatePassword: user.needsToUpdatePassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ const authenticateToken = async (
|
|||||||
username: 'desktopModeUsername',
|
username: 'desktopModeUsername',
|
||||||
displayName: 'desktopModeDisplayName',
|
displayName: 'desktopModeDisplayName',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
req.accessToken = 'desktopModeAccessToken'
|
req.accessToken = 'desktopModeAccessToken'
|
||||||
return next()
|
return next()
|
||||||
|
|||||||
@@ -33,5 +33,6 @@ export const desktopUser: RequestUser = {
|
|||||||
username: userInfo().username,
|
username: userInfo().username,
|
||||||
displayName: userInfo().username,
|
displayName: userInfo().username,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import mongoose, { Schema } from 'mongoose'
|
import mongoose, { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
export const NUMBER_OF_SECONDS_IN_A_DAY = 86400
|
||||||
export interface ClientPayload {
|
export interface ClientPayload {
|
||||||
/**
|
/**
|
||||||
* Client ID
|
* Client ID
|
||||||
@@ -11,6 +12,16 @@ export interface ClientPayload {
|
|||||||
* @example "someRandomCryptoString"
|
* @example "someRandomCryptoString"
|
||||||
*/
|
*/
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
|
/**
|
||||||
|
* Number of seconds after which access token will expire. Default is 86400 (1 day)
|
||||||
|
* @example 86400
|
||||||
|
*/
|
||||||
|
accessTokenExpiration?: number
|
||||||
|
/**
|
||||||
|
* Number of seconds after which access token will expire. Default is 2592000 (30 days)
|
||||||
|
* @example 2592000
|
||||||
|
*/
|
||||||
|
refreshTokenExpiration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientSchema = new Schema<ClientPayload>({
|
const ClientSchema = new Schema<ClientPayload>({
|
||||||
@@ -21,6 +32,14 @@ const ClientSchema = new Schema<ClientPayload>({
|
|||||||
clientSecret: {
|
clientSecret: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
accessTokenExpiration: {
|
||||||
|
type: Number,
|
||||||
|
default: NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
|
},
|
||||||
|
refreshTokenExpiration: {
|
||||||
|
type: Number,
|
||||||
|
default: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ interface IUserDocument extends UserPayload, Document {
|
|||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
autoExec: string
|
autoExec: string
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
@@ -81,6 +82,10 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
needsToUpdatePassword: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
autoExec: {
|
autoExec: {
|
||||||
type: String
|
type: String
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,12 +7,28 @@ import {
|
|||||||
authenticateRefreshToken
|
authenticateRefreshToken
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
import { tokenValidation } from '../../utils'
|
import { tokenValidation, updatePasswordValidation } from '../../utils'
|
||||||
import { InfoJWT } from '../../types'
|
import { InfoJWT } from '../../types'
|
||||||
|
|
||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
const controller = new AuthController()
|
const controller = new AuthController()
|
||||||
|
|
||||||
|
authRouter.patch(
|
||||||
|
'/updatePassword',
|
||||||
|
authenticateAccessToken,
|
||||||
|
async (req, res) => {
|
||||||
|
const { error, value: body } = updatePasswordValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await controller.updatePassword(req, body)
|
||||||
|
res.sendStatus(204)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(err.code).send(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
authRouter.post('/token', async (req, res) => {
|
authRouter.post('/token', async (req, res) => {
|
||||||
const { error, value: body } = tokenValidation(req.body)
|
const { error, value: body } = tokenValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ describe('web', () => {
|
|||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin
|
isAdmin: user.isAdmin,
|
||||||
|
needsToUpdatePassword: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const publishAppStream = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const sasJsPort = process.env.PORT || 5000
|
const sasJsPort = process.env.PORT || 5000
|
||||||
console.log(
|
process.logger.info(
|
||||||
'Serving Stream App: ',
|
'Serving Stream App: ',
|
||||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
|
|||||||
appStreamRouter(req, res, next)
|
appStreamRouter(req, res, next)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/', csrfProtection, webRouter)
|
app.use('/', webRouter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import sas9WebRouter from './sas9-web'
|
|||||||
import sasViyaWebRouter from './sasviya-web'
|
import sasViyaWebRouter from './sasviya-web'
|
||||||
import webRouter from './web'
|
import webRouter from './web'
|
||||||
import { MOCK_SERVERTYPEType } from '../../utils'
|
import { MOCK_SERVERTYPEType } from '../../utils'
|
||||||
|
import { csrfProtection } from '../../middlewares'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ switch (MOCK_SERVERTYPE) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
router.use('/', webRouter)
|
router.use('/', csrfProtection, webRouter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,25 @@ import express from 'express'
|
|||||||
import { generateCSRFToken } from '../../middlewares'
|
import { generateCSRFToken } from '../../middlewares'
|
||||||
import { WebController } from '../../controllers'
|
import { WebController } from '../../controllers'
|
||||||
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||||
|
import multer from 'multer'
|
||||||
|
import path from 'path'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { FileUploadController } from '../../controllers/internal'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
const sas9WebRouter = express.Router()
|
const sas9WebRouter = express.Router()
|
||||||
const webController = new WebController()
|
const webController = new WebController()
|
||||||
// Mock controller must be singleton because it keeps the states
|
// Mock controller must be singleton because it keeps the states
|
||||||
// for example `isLoggedIn` and potentially more in future mocks
|
// for example `isLoggedIn` and potentially more in future mocks
|
||||||
const controller = new MockSas9Controller()
|
const controller = new MockSas9Controller()
|
||||||
|
const fileUploadController = new FileUploadController()
|
||||||
|
|
||||||
|
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
|
||||||
|
})
|
||||||
|
|
||||||
sas9WebRouter.get('/', async (req, res) => {
|
sas9WebRouter.get('/', async (req, res) => {
|
||||||
let response
|
let response
|
||||||
@@ -27,7 +40,7 @@ sas9WebRouter.get('/', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||||
const response = await controller.sasStoredProcess()
|
const response = await controller.sasStoredProcess(req)
|
||||||
|
|
||||||
if (response.redirect) {
|
if (response.redirect) {
|
||||||
res.redirect(response.redirect)
|
res.redirect(response.redirect)
|
||||||
@@ -41,8 +54,8 @@ sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
|
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
|
||||||
const response = await controller.sasStoredProcessDo(req)
|
const response = await controller.sasStoredProcessDoGet(req)
|
||||||
|
|
||||||
if (response.redirect) {
|
if (response.redirect) {
|
||||||
res.redirect(response.redirect)
|
res.redirect(response.redirect)
|
||||||
@@ -56,6 +69,26 @@ sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post(
|
||||||
|
'/SASStoredProcess/do/',
|
||||||
|
fileUploadController.preUploadMiddleware,
|
||||||
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
|
async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcessDoPost(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
||||||
const response = await controller.loginGet()
|
const response = await controller.loginGet()
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ appPromise.then(async (app) => {
|
|||||||
const protocol = process.env.PROTOCOL || 'http'
|
const protocol = process.env.PROTOCOL || 'http'
|
||||||
const sasJsPort = process.env.PORT || 5000
|
const sasJsPort = process.env.PORT || 5000
|
||||||
|
|
||||||
console.log('PROTOCOL: ', protocol)
|
process.logger.info('PROTOCOL: ', protocol)
|
||||||
|
|
||||||
if (protocol !== 'https') {
|
if (protocol !== 'https') {
|
||||||
app.listen(sasJsPort, () => {
|
app.listen(sasJsPort, () => {
|
||||||
console.log(
|
process.logger.info(
|
||||||
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -20,7 +20,7 @@ appPromise.then(async (app) => {
|
|||||||
|
|
||||||
const httpsServer = createServer({ key, cert, ca }, app)
|
const httpsServer = createServer({ key, cert, ca }, app)
|
||||||
httpsServer.listen(sasJsPort, () => {
|
httpsServer.listen(sasJsPort, () => {
|
||||||
console.log(
|
process.logger.info(
|
||||||
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export interface RequestUser {
|
|||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
autoExec?: string
|
autoExec?: string
|
||||||
}
|
}
|
||||||
|
|||||||
2
api/src/types/system/process.d.ts
vendored
2
api/src/types/system/process.d.ts
vendored
@@ -5,9 +5,11 @@ declare namespace NodeJS {
|
|||||||
pythonLoc?: string
|
pythonLoc?: string
|
||||||
rLoc?: string
|
rLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
|
sasjsRoot: string
|
||||||
logsLoc: string
|
logsLoc: string
|
||||||
logsUUID: string
|
logsUUID: string
|
||||||
sessionController?: import('../../controllers/internal').SessionController
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
|
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||||
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[]
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const loadAppStreamConfig = async () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('App Stream Config loaded!')
|
process.logger.info('App Stream Config loaded!')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addEntryToAppStreamConfig = (
|
export const addEntryToAppStreamConfig = (
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export const connectDB = async () => {
|
|||||||
throw new Error('Unable to connect to DB!')
|
throw new Error('Unable to connect to DB!')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Connected to DB!')
|
process.logger.success('Connected to DB!')
|
||||||
return seedDB()
|
return seedDB()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
|||||||
export const copySASjsCore = async () => {
|
export const copySASjsCore = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
console.log('Copying Macros from container to drive(tmp).')
|
process.logger.log('Copying Macros from container to drive.')
|
||||||
|
|
||||||
const macrosDrivePath = getMacrosFolder()
|
const macrosDrivePath = getMacrosFolder()
|
||||||
|
|
||||||
@@ -30,5 +30,5 @@ export const copySASjsCore = async () => {
|
|||||||
await createFile(macroFileDestPath, macroContent)
|
await createFile(macroFileDestPath, macroContent)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Macros Drive Path:', macrosDrivePath)
|
process.logger.info('Macros Drive Path:', macrosDrivePath)
|
||||||
}
|
}
|
||||||
|
|||||||
18
api/src/utils/createWeboutSasFile.ts
Normal file
18
api/src/utils/createWeboutSasFile.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { createFile } from '@sasjs/utils'
|
||||||
|
import { getMacrosFolder } from './file'
|
||||||
|
|
||||||
|
const fileContent = `%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO,maxobs=MAX);
|
||||||
|
%ms_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt
|
||||||
|
,missing=&missing
|
||||||
|
,showmeta=&showmeta
|
||||||
|
,maxobs=&maxobs
|
||||||
|
)
|
||||||
|
%mend;`
|
||||||
|
|
||||||
|
export const createWeboutSasFile = async () => {
|
||||||
|
const macrosDrivePath = getMacrosFolder()
|
||||||
|
process.logger.log(`Creating webout.sas at ${macrosDrivePath}`)
|
||||||
|
const filePath = path.join(macrosDrivePath, 'webout.sas')
|
||||||
|
await createFile(filePath, fileContent)
|
||||||
|
}
|
||||||
@@ -20,22 +20,24 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
|||||||
export const getDesktopUserAutoExecPath = () =>
|
export const getDesktopUserAutoExecPath = () =>
|
||||||
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
||||||
|
|
||||||
export const getSasjsRootFolder = () => process.driveLoc
|
export const getSasjsRootFolder = () => process.sasjsRoot
|
||||||
|
|
||||||
|
export const getSasjsDriveFolder = () => process.driveLoc
|
||||||
|
|
||||||
export const getLogFolder = () => process.logsLoc
|
export const getLogFolder = () => process.logsLoc
|
||||||
|
|
||||||
export const getAppStreamConfigPath = () =>
|
export const getAppStreamConfigPath = () =>
|
||||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
export const getMacrosFolder = () =>
|
export const getMacrosFolder = () =>
|
||||||
path.join(getSasjsRootFolder(), 'sas', 'sasautos')
|
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
|
||||||
|
|
||||||
export const getPackagesFolder = () =>
|
export const getPackagesFolder = () =>
|
||||||
path.join(getSasjsRootFolder(), 'sas', 'sas_packages')
|
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
|
||||||
|
|
||||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||||
|
|
||||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
|
||||||
|
|
||||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
|
||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT, expiry?: number) =>
|
||||||
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
||||||
expiresIn: '1day'
|
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
|
||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT, expiry?: number) =>
|
||||||
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
||||||
expiresIn: '30 days'
|
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export const getCertificates = async () => {
|
|||||||
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
|
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
|
||||||
const caPath = CA_ROOT
|
const caPath = CA_ROOT
|
||||||
|
|
||||||
console.log('keyPath: ', keyPath)
|
process.logger.info('keyPath: ', keyPath)
|
||||||
console.log('certPath: ', certPath)
|
process.logger.info('certPath: ', certPath)
|
||||||
if (caPath) console.log('caPath: ', caPath)
|
if (caPath) process.logger.info('caPath: ', caPath)
|
||||||
|
|
||||||
const key = await readFile(keyPath)
|
const key = await readFile(keyPath)
|
||||||
const cert = await readFile(certPath)
|
const cert = await readFile(certPath)
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
|||||||
|
|
||||||
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||||
|
|
||||||
|
//In desktop mode when mocking mode is enabled, user was undefined.
|
||||||
|
//So this is workaround.
|
||||||
return {
|
return {
|
||||||
username: user!.username,
|
username: user ? user.username : 'demo',
|
||||||
userId: user!.userId,
|
userId: user ? user.userId : 0,
|
||||||
displayName: user!.displayName,
|
displayName: user ? user.displayName : 'demo',
|
||||||
serverUrl: protocol + host,
|
serverUrl: protocol + host,
|
||||||
httpHeaders
|
httpHeaders
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './appStreamConfig'
|
export * from './appStreamConfig'
|
||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
export * from './copySASjsCore'
|
export * from './copySASjsCore'
|
||||||
|
export * from './createWeboutSasFile'
|
||||||
export * from './desktopAutoExec'
|
export * from './desktopAutoExec'
|
||||||
export * from './extractHeaders'
|
export * from './extractHeaders'
|
||||||
export * from './extractName'
|
export * from './extractName'
|
||||||
@@ -26,6 +27,7 @@ export * from './saveTokensInDB'
|
|||||||
export * from './seedDB'
|
export * from './seedDB'
|
||||||
export * from './setProcessVariables'
|
export * from './setProcessVariables'
|
||||||
export * from './setupFolders'
|
export * from './setupFolders'
|
||||||
|
export * from './setupUserAutoExec'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyEnvVariables'
|
export * from './verifyEnvVariables'
|
||||||
|
|||||||
@@ -27,5 +27,6 @@ export const publicUser: RequestUser = {
|
|||||||
username: 'publicUser',
|
username: 'publicUser',
|
||||||
displayName: 'Public User',
|
displayName: 'Public User',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ export const getEnvCSPDirectives = (
|
|||||||
try {
|
try {
|
||||||
cspConfigJson = JSON.parse(file)
|
cspConfigJson = JSON.parse(file)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
process.logger.error(
|
||||||
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
|
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error reading HELMET CSP config file', e)
|
process.logger.error('Error reading HELMET CSP config file', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
const client = new Client(CLIENT)
|
const client = new Client(CLIENT)
|
||||||
await client.save()
|
await client.save()
|
||||||
|
|
||||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
process.logger.success(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if 'AllUsers' Group is already in the database
|
// Checking if 'AllUsers' Group is already in the database
|
||||||
@@ -28,7 +28,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
const group = new Group(GROUP)
|
const group = new Group(GROUP)
|
||||||
groupExist = await group.save()
|
groupExist = await group.save()
|
||||||
|
|
||||||
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
process.logger.success(`DB Seed - Group created: ${GROUP.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if 'Public' Group is already in the database
|
// Checking if 'Public' Group is already in the database
|
||||||
@@ -37,7 +37,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
const group = new Group(PUBLIC_GROUP)
|
const group = new Group(PUBLIC_GROUP)
|
||||||
await group.save()
|
await group.save()
|
||||||
|
|
||||||
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
@@ -46,12 +46,14 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
const user = new User(ADMIN_USER)
|
const user = new User(ADMIN_USER)
|
||||||
usernameExist = await user.save()
|
usernameExist = await user.save()
|
||||||
|
|
||||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
process.logger.success(
|
||||||
|
`DB Seed - admin account created: ${ADMIN_USER.username}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupExist.hasUser(usernameExist)) {
|
if (!groupExist.hasUser(usernameExist)) {
|
||||||
groupExist.addUser(usernameExist)
|
groupExist.addUser(usernameExist)
|
||||||
console.log(
|
process.logger.success(
|
||||||
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
|
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -62,7 +64,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
const configuration = new Configuration(SECRETS)
|
const configuration = new Configuration(SECRETS)
|
||||||
configExist = await configuration.save()
|
configExist = await configuration.save()
|
||||||
|
|
||||||
console.log('DB Seed - configuration added')
|
process.logger.success('DB Seed - configuration added')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export const setProcessVariables = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
|
||||||
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +33,6 @@ export const setProcessVariables = async () => {
|
|||||||
process.rLoc = process.env.R_PATH
|
process.rLoc = process.env.R_PATH
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
process.sasLoc = sasLoc
|
||||||
process.nodeLoc = nodeLoc
|
process.nodeLoc = nodeLoc
|
||||||
process.pythonLoc = pythonLoc
|
process.pythonLoc = pythonLoc
|
||||||
@@ -42,11 +42,19 @@ export const setProcessVariables = async () => {
|
|||||||
const { SASJS_ROOT } = process.env
|
const { SASJS_ROOT } = process.env
|
||||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||||
await createFolder(absPath)
|
await createFolder(absPath)
|
||||||
process.driveLoc = getRealPath(absPath)
|
process.sasjsRoot = getRealPath(absPath)
|
||||||
|
|
||||||
|
const { DRIVE_LOCATION } = process.env
|
||||||
|
const absDrivePath = getAbsolutePath(
|
||||||
|
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
|
||||||
|
process.cwd()
|
||||||
|
)
|
||||||
|
await createFolder(absDrivePath)
|
||||||
|
process.driveLoc = getRealPath(absDrivePath)
|
||||||
|
|
||||||
const { LOG_LOCATION } = process.env
|
const { LOG_LOCATION } = process.env
|
||||||
const absLogsPath = getAbsolutePath(
|
const absLogsPath = getAbsolutePath(
|
||||||
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
|
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
|
||||||
process.cwd()
|
process.cwd()
|
||||||
)
|
)
|
||||||
await createFolder(absLogsPath)
|
await createFolder(absLogsPath)
|
||||||
@@ -54,8 +62,8 @@ export const setProcessVariables = async () => {
|
|||||||
|
|
||||||
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
process.logger.info('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
process.logger.info('sasDrive: ', process.driveLoc)
|
||||||
console.log('sasLogs: ', process.logsLoc)
|
process.logger.info('sasLogs: ', process.logsLoc)
|
||||||
console.log('runTimes: ', process.runTimes)
|
process.logger.info('runTimes: ', process.runTimes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
import { createFolder } from '@sasjs/utils'
|
||||||
import {
|
import { getFilesFolder, getPackagesFolder } from './file'
|
||||||
getDesktopUserAutoExecPath,
|
|
||||||
getFilesFolder,
|
|
||||||
getPackagesFolder
|
|
||||||
} from './file'
|
|
||||||
import { ModeType } from './verifyEnvVariables'
|
|
||||||
|
|
||||||
export const setupFolders = async () => {
|
export const setupFilesFolder = async () => await createFolder(getFilesFolder())
|
||||||
const drivePath = getFilesFolder()
|
|
||||||
await createFolder(drivePath)
|
export const setupPackagesFolder = async () =>
|
||||||
await createFolder(getPackagesFolder())
|
await createFolder(getPackagesFolder())
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Desktop) {
|
|
||||||
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
|
||||||
await createFile(getDesktopUserAutoExecPath(), '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
11
api/src/utils/setupUserAutoExec.ts
Normal file
11
api/src/utils/setupUserAutoExec.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createFile, fileExists } from '@sasjs/utils'
|
||||||
|
import { getDesktopUserAutoExecPath } from './file'
|
||||||
|
import { ModeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
|
export const setupUserAutoExec = async () => {
|
||||||
|
if (process.env.MODE === ModeType.Desktop) {
|
||||||
|
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||||
|
await createFile(getDesktopUserAutoExecPath(), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,10 +85,18 @@ export const updateUserValidation = (
|
|||||||
return Joi.object(validationChecks).validate(data)
|
return Joi.object(validationChecks).validate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updatePasswordValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
currentPassword: Joi.string().required(),
|
||||||
|
newPassword: passwordSchema.required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
clientId: Joi.string().required(),
|
clientId: Joi.string().required(),
|
||||||
clientSecret: Joi.string().required()
|
clientSecret: Joi.string().required(),
|
||||||
|
accessTokenExpiration: Joi.number(),
|
||||||
|
refreshTokenExpiration: Joi.number()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const fetchLatestAutoExec = async (
|
|||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive,
|
isActive: dbUser.isActive,
|
||||||
|
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||||
autoExec: dbUser.autoExec
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ export const verifyTokenInDB = async (
|
|||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive,
|
isActive: dbUser.isActive,
|
||||||
|
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||||
autoExec: dbUser.autoExec
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
680
web/package-lock.json
generated
680
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import Header from './components/header'
|
|||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
import Settings from './containers/Settings'
|
import Settings from './containers/Settings'
|
||||||
|
import UpdatePassword from './components/updatePassword'
|
||||||
|
|
||||||
import { AppContext } from './context/appContext'
|
import { AppContext } from './context/appContext'
|
||||||
import AuthCode from './containers/AuthCode'
|
import AuthCode from './containers/AuthCode'
|
||||||
@@ -29,6 +30,20 @@ function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appContext.needsToUpdatePassword) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<HashRouter>
|
||||||
|
<Header />
|
||||||
|
<Routes>
|
||||||
|
<Route path="*" element={<UpdatePassword />} />
|
||||||
|
</Routes>
|
||||||
|
<ToastContainer />
|
||||||
|
</HashRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
|||||||
@@ -31,14 +31,24 @@ const DeleteConfirmationModal = ({
|
|||||||
message,
|
message,
|
||||||
_delete
|
_delete
|
||||||
}: DeleteConfirmationModalProps) => {
|
}: DeleteConfirmationModalProps) => {
|
||||||
|
const handleDeleteClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
_delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog onClose={handleClose} open={open}>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography gutterBottom>{message}</Typography>
|
<Typography gutterBottom>{message}</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
<Button color="error" onClick={() => _delete()}>
|
<Button color="error" onClick={handleDeleteClick}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Box from '@mui/material/Box'
|
|||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
<Box className="main">
|
<Box className="container">
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<h2>Welcome to SASjs Server!</h2>
|
<h2>Welcome to SASjs Server!</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ import axios from 'axios'
|
|||||||
import React, { useState, useContext } from 'react'
|
import React, { useState, useContext } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import { CssBaseline, Box, TextField, Button } from '@mui/material'
|
import {
|
||||||
|
Backdrop,
|
||||||
|
CircularProgress,
|
||||||
|
CssBaseline,
|
||||||
|
Box,
|
||||||
|
TextField,
|
||||||
|
Button
|
||||||
|
} from '@mui/material'
|
||||||
import { AppContext } from '../context/appContext'
|
import { AppContext } from '../context/appContext'
|
||||||
|
|
||||||
const login = async (payload: { username: string; password: string }) =>
|
const login = async (payload: { username: string; password: string }) =>
|
||||||
@@ -10,21 +17,27 @@ const login = async (payload: { username: string; password: string }) =>
|
|||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
|
setIsLoading(true)
|
||||||
setErrorMessage('')
|
setErrorMessage('')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const { loggedIn, user } = await login({
|
const { loggedIn, user } = await login({
|
||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
}).catch((err: any) => {
|
|
||||||
setErrorMessage(err.response?.data || err.toString())
|
|
||||||
return {}
|
|
||||||
})
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setErrorMessage(err.response?.data || err.toString())
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
appContext.setUserId?.(user.id)
|
appContext.setUserId?.(user.id)
|
||||||
@@ -32,46 +45,56 @@ const Login = () => {
|
|||||||
appContext.setDisplayName?.(user.displayName)
|
appContext.setDisplayName?.(user.displayName)
|
||||||
appContext.setIsAdmin?.(user.isAdmin)
|
appContext.setIsAdmin?.(user.isAdmin)
|
||||||
appContext.setLoggedIn?.(loggedIn)
|
appContext.setLoggedIn?.(loggedIn)
|
||||||
|
appContext.setNeedsToUpdatePassword?.(user.needsToUpdatePassword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
className="main"
|
<Backdrop
|
||||||
component="form"
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
onSubmit={handleSubmit}
|
open={isLoading}
|
||||||
sx={{
|
|
||||||
'& > :not(style)': { m: 1, width: '25ch' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CssBaseline />
|
|
||||||
<br />
|
|
||||||
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2>
|
|
||||||
<TextField
|
|
||||||
id="username"
|
|
||||||
label="Username"
|
|
||||||
type="text"
|
|
||||||
variant="outlined"
|
|
||||||
onChange={(e: any) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="password"
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
variant="outlined"
|
|
||||||
onChange={(e: any) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{errorMessage && <span>{errorMessage}</span>}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="outlined"
|
|
||||||
disabled={!appContext.setLoggedIn}
|
|
||||||
>
|
>
|
||||||
Submit
|
<CircularProgress color="inherit" />
|
||||||
</Button>
|
</Backdrop>
|
||||||
</Box>
|
|
||||||
|
<Box
|
||||||
|
className="container"
|
||||||
|
component="form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
sx={{
|
||||||
|
'& > :not(style)': { m: 1, width: '25ch' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CssBaseline />
|
||||||
|
<br />
|
||||||
|
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2>
|
||||||
|
<TextField
|
||||||
|
id="username"
|
||||||
|
label="Username"
|
||||||
|
type="text"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={(e: any) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={(e: any) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{errorMessage && <span>{errorMessage}</span>}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!appContext.setLoggedIn}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,18 @@ const NameInputModal = ({
|
|||||||
action(name)
|
action(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleActionClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
action(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog fullWidth onClose={handleClose} open={open}>
|
||||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
{title}
|
{title}
|
||||||
</BootstrapDialogTitle>
|
</BootstrapDialogTitle>
|
||||||
@@ -91,12 +101,12 @@ const NameInputModal = ({
|
|||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
<Button variant="contained" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => action(name)}
|
onClick={handleActionClick}
|
||||||
disabled={hasError || !name}
|
disabled={hasError || !name}
|
||||||
>
|
>
|
||||||
{actionLabel}
|
{actionLabel}
|
||||||
|
|||||||
145
web/src/components/passwordModal.tsx
Normal file
145
web/src/components/passwordModal.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
OutlinedInput,
|
||||||
|
InputAdornment,
|
||||||
|
IconButton,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
FormHelperText
|
||||||
|
} from '@mui/material'
|
||||||
|
import Visibility from '@mui/icons-material/Visibility'
|
||||||
|
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||||
|
|
||||||
|
import { BootstrapDialogTitle } from './dialogTitle'
|
||||||
|
import { BootstrapDialog } from './modal'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
title: string
|
||||||
|
updatePassword: (currentPassword: string, newPassword: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdatePasswordModal = (props: Props) => {
|
||||||
|
const { open, setOpen, title, updatePassword } = props
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('')
|
||||||
|
const [newPassword, setNewPassword] = useState('')
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const [errorText, setErrorText] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
currentPassword.length > 0 &&
|
||||||
|
newPassword.length > 0 &&
|
||||||
|
newPassword === currentPassword
|
||||||
|
) {
|
||||||
|
setErrorText('New password should be different to current password.')
|
||||||
|
setHasError(true)
|
||||||
|
} else if (newPassword.length >= 6) {
|
||||||
|
setErrorText('')
|
||||||
|
setHasError(false)
|
||||||
|
}
|
||||||
|
}, [currentPassword, newPassword])
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
setErrorText('Password length should be at least 6 characters.')
|
||||||
|
setHasError(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||||
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
|
{title}
|
||||||
|
</BootstrapDialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<PasswordInput
|
||||||
|
label="Current Password"
|
||||||
|
password={currentPassword}
|
||||||
|
setPassword={setCurrentPassword}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<PasswordInput
|
||||||
|
label="New Password"
|
||||||
|
password={newPassword}
|
||||||
|
setPassword={setNewPassword}
|
||||||
|
hasError={hasError}
|
||||||
|
errorText={errorText}
|
||||||
|
handleBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => updatePassword(currentPassword, newPassword)}
|
||||||
|
disabled={hasError || !currentPassword || !newPassword}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</BootstrapDialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdatePasswordModal
|
||||||
|
|
||||||
|
type PasswordInputProps = {
|
||||||
|
label: string
|
||||||
|
password: string
|
||||||
|
setPassword: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
hasError?: boolean
|
||||||
|
errorText?: string
|
||||||
|
handleBlur?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordInput = ({
|
||||||
|
label,
|
||||||
|
password,
|
||||||
|
setPassword,
|
||||||
|
hasError,
|
||||||
|
errorText,
|
||||||
|
handleBlur
|
||||||
|
}: PasswordInputProps) => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl sx={{ width: '100%' }} variant="outlined" error={hasError}>
|
||||||
|
<InputLabel htmlFor="outlined-adornment-password">{label}</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="outlined-adornment-password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
label={label}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowPassword((val) => !val)}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{errorText && <FormHelperText>{errorText}</FormHelperText>}
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,67 +1,79 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Menu, MenuItem } from '@mui/material'
|
import { Menu, MenuItem, Typography } from '@mui/material'
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||||
|
import MuiTreeView from '@mui/lab/TreeView'
|
||||||
|
import MuiTreeItem from '@mui/lab/TreeItem'
|
||||||
|
|
||||||
import DeleteConfirmationModal from './deleteConfirmationModal'
|
import DeleteConfirmationModal from './deleteConfirmationModal'
|
||||||
import NameInputModal from './nameInputModal'
|
import NameInputModal from './nameInputModal'
|
||||||
|
|
||||||
import { TreeNode } from '../utils/types'
|
import { TreeNode } from '../utils/types'
|
||||||
|
|
||||||
type Props = {
|
interface Props {
|
||||||
node: TreeNode
|
node: TreeNode
|
||||||
selectedFilePath: string
|
|
||||||
handleSelect: (filePath: string) => void
|
handleSelect: (filePath: string) => void
|
||||||
deleteNode: (path: string, isFolder: boolean) => void
|
deleteNode: (path: string, isFolder: boolean) => void
|
||||||
addFile: (path: string) => void
|
addFile: (path: string) => void
|
||||||
addFolder: (path: string) => void
|
addFolder: (path: string) => void
|
||||||
rename: (oldPath: string, newPath: string) => void
|
rename: (oldPath: string, newPath: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeViewProps extends Props {
|
||||||
defaultExpanded?: string[]
|
defaultExpanded?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const TreeView = ({
|
const TreeView = ({
|
||||||
node,
|
node,
|
||||||
selectedFilePath,
|
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
addFile,
|
addFile,
|
||||||
addFolder,
|
addFolder,
|
||||||
rename,
|
rename,
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
}: Props) => {
|
}: TreeViewProps) => {
|
||||||
return (
|
const renderTree = (nodes: TreeNode) => (
|
||||||
<ul
|
<MuiTreeItem
|
||||||
style={{
|
key={nodes.relativePath}
|
||||||
listStyle: 'none',
|
nodeId={nodes.relativePath}
|
||||||
padding: '0.25rem 0.85rem',
|
label={
|
||||||
width: 'max-content'
|
<TreeItemWithContextMenu
|
||||||
}}
|
node={nodes}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
|
rename={rename}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TreeViewNode
|
{Array.isArray(nodes.children)
|
||||||
node={node}
|
? nodes.children.map((node) => renderTree(node))
|
||||||
selectedFilePath={selectedFilePath}
|
: null}
|
||||||
handleSelect={handleSelect}
|
</MuiTreeItem>
|
||||||
deleteNode={deleteNode}
|
)
|
||||||
addFile={addFile}
|
|
||||||
addFolder={addFolder}
|
return (
|
||||||
rename={rename}
|
<MuiTreeView
|
||||||
defaultExpanded={defaultExpanded}
|
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||||
/>
|
defaultExpandIcon={<ChevronRightIcon />}
|
||||||
</ul>
|
defaultExpanded={defaultExpanded}
|
||||||
|
sx={{ flexGrow: 1, maxWidth: 400, overflowY: 'auto' }}
|
||||||
|
>
|
||||||
|
{renderTree(node)}
|
||||||
|
</MuiTreeView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TreeView
|
export default TreeView
|
||||||
|
|
||||||
const TreeViewNode = ({
|
const TreeItemWithContextMenu = ({
|
||||||
node,
|
node,
|
||||||
selectedFilePath,
|
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
addFile,
|
addFile,
|
||||||
addFolder,
|
addFolder,
|
||||||
rename,
|
rename
|
||||||
defaultExpanded
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
@@ -72,18 +84,19 @@ const TreeViewNode = ({
|
|||||||
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
||||||
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
||||||
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||||
const [childVisible, setChildVisibility] = useState(false)
|
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
mouseX: number
|
mouseX: number
|
||||||
mouseY: number
|
mouseY: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const launchProgram = () => {
|
const launchProgram = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchProgramWithDebug = () => {
|
const launchProgramWithDebug = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
window.open(
|
window.open(
|
||||||
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
||||||
@@ -103,25 +116,18 @@ const TreeViewNode = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChild = node.children.length ? true : false
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
const handleItemClick = () => {
|
setContextMenu(null)
|
||||||
if (node.children.length) {
|
}
|
||||||
setChildVisibility((v) => !v)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleItemClick = (event: React.MouseEvent) => {
|
||||||
|
if (node.children.length) return
|
||||||
handleSelect(node.relativePath)
|
handleSelect(node.relativePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const handleDeleteItemClick = (event: React.MouseEvent) => {
|
||||||
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
|
event.stopPropagation()
|
||||||
setChildVisibility(true)
|
|
||||||
defaultExpanded.shift()
|
|
||||||
}
|
|
||||||
}, [defaultExpanded, node.relativePath])
|
|
||||||
|
|
||||||
const handleDeleteItemClick = () => {
|
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setDeleteConfirmationModalOpen(true)
|
setDeleteConfirmationModalOpen(true)
|
||||||
setDeleteConfirmationModalMessage(
|
setDeleteConfirmationModalMessage(
|
||||||
@@ -136,7 +142,8 @@ const TreeViewNode = ({
|
|||||||
deleteNode(node.relativePath, node.isFolder)
|
deleteNode(node.relativePath, node.isFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewFolderItemClick = () => {
|
const handleNewFolderItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Add Folder')
|
setNameInputModalTitle('Add Folder')
|
||||||
@@ -145,7 +152,8 @@ const TreeViewNode = ({
|
|||||||
setDefaultInputModalName('')
|
setDefaultInputModalName('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewFileItemClick = () => {
|
const handleNewFileItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Add File')
|
setNameInputModalTitle('Add File')
|
||||||
@@ -161,7 +169,8 @@ const TreeViewNode = ({
|
|||||||
else addFile(path)
|
else addFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRenameItemClick = () => {
|
const handleRenameItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Rename')
|
setNameInputModalTitle('Rename')
|
||||||
@@ -181,34 +190,7 @@ const TreeViewNode = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||||
<li style={{ display: 'list-item' }}>
|
<Typography onClick={handleItemClick}>{node.name}</Typography>
|
||||||
<div
|
|
||||||
className={`tree-item-label ${
|
|
||||||
selectedFilePath === node.relativePath ? 'selected' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => handleItemClick()}
|
|
||||||
>
|
|
||||||
{hasChild &&
|
|
||||||
(childVisible ? <ExpandMoreIcon /> : <ChevronRightIcon />)}
|
|
||||||
<div>{node.name}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasChild &&
|
|
||||||
childVisible &&
|
|
||||||
node.children.map((child, index) => (
|
|
||||||
<TreeView
|
|
||||||
key={node.relativePath + '-' + index}
|
|
||||||
node={child}
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
handleSelect={handleSelect}
|
|
||||||
deleteNode={deleteNode}
|
|
||||||
addFile={addFile}
|
|
||||||
addFolder={addFolder}
|
|
||||||
rename={rename}
|
|
||||||
defaultExpanded={defaultExpanded}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</li>
|
|
||||||
<DeleteConfirmationModal
|
<DeleteConfirmationModal
|
||||||
open={deleteConfirmationModalOpen}
|
open={deleteConfirmationModalOpen}
|
||||||
setOpen={setDeleteConfirmationModalOpen}
|
setOpen={setDeleteConfirmationModalOpen}
|
||||||
@@ -228,7 +210,7 @@ const TreeViewNode = ({
|
|||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
open={contextMenu !== null}
|
open={contextMenu !== null}
|
||||||
onClose={() => setContextMenu(null)}
|
onClose={handleClose}
|
||||||
anchorReference="anchorPosition"
|
anchorReference="anchorPosition"
|
||||||
anchorPosition={
|
anchorPosition={
|
||||||
contextMenu !== null
|
contextMenu !== null
|
||||||
|
|||||||
109
web/src/components/updatePassword.tsx
Normal file
109
web/src/components/updatePassword.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Box, CssBaseline, Button, CircularProgress } from '@mui/material'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { PasswordInput } from './passwordModal'
|
||||||
|
|
||||||
|
import { AppContext } from '../context/appContext'
|
||||||
|
|
||||||
|
const UpdatePassword = () => {
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('')
|
||||||
|
const [newPassword, setNewPassword] = useState('')
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const [errorText, setErrorText] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
currentPassword.length > 0 &&
|
||||||
|
newPassword.length > 0 &&
|
||||||
|
newPassword === currentPassword
|
||||||
|
) {
|
||||||
|
setErrorText('New password should be different to current password.')
|
||||||
|
setHasError(true)
|
||||||
|
} else if (newPassword.length >= 6) {
|
||||||
|
setErrorText('')
|
||||||
|
setHasError(false)
|
||||||
|
}
|
||||||
|
}, [currentPassword, newPassword])
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
setErrorText('Password length should be at least 6 characters.')
|
||||||
|
setHasError(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (hasError || !currentPassword || !newPassword) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.patch(`/SASjsApi/auth/updatePassword`, {
|
||||||
|
currentPassword,
|
||||||
|
newPassword
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
appContext.setNeedsToUpdatePassword?.(false)
|
||||||
|
toast.success('Password updated', {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed: ' + err.response?.data || err.text, {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLoading ? (
|
||||||
|
<CircularProgress
|
||||||
|
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
className="container"
|
||||||
|
component="form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
sx={{
|
||||||
|
'& > :not(style)': { m: 1, width: '25ch' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CssBaseline />
|
||||||
|
<h2>Welcome to SASjs Server!</h2>
|
||||||
|
<p style={{ width: 'auto' }}>
|
||||||
|
This is your first time login to SASjs server. Therefore, you need to
|
||||||
|
update your password.
|
||||||
|
</p>
|
||||||
|
<PasswordInput
|
||||||
|
label="Current Password"
|
||||||
|
password={currentPassword}
|
||||||
|
setPassword={setCurrentPassword}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="New Password"
|
||||||
|
password={newPassword}
|
||||||
|
setPassword={setNewPassword}
|
||||||
|
hasError={hasError}
|
||||||
|
errorText={errorText}
|
||||||
|
handleBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outlined"
|
||||||
|
disabled={hasError || !currentPassword || !newPassword}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdatePassword
|
||||||
@@ -47,7 +47,7 @@ const AuthCode = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="main">
|
<Box className="container">
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<br />
|
<br />
|
||||||
<h2>Authorization Code</h2>
|
<h2>Authorization Code</h2>
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import {
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
import { AppContext, ModeType } from '../../context/appContext'
|
import { AppContext, ModeType } from '../../context/appContext'
|
||||||
|
import UpdatePasswordModal from '../../components/passwordModal'
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [user, setUser] = useState({} as any)
|
const [user, setUser] = useState({} as any)
|
||||||
|
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
@@ -36,7 +38,7 @@ const Profile = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [appContext.userId])
|
||||||
|
|
||||||
const handleChange = (event: any) => {
|
const handleChange = (event: any) => {
|
||||||
const { name, value } = event.target
|
const { name, value } = event.target
|
||||||
@@ -68,82 +70,124 @@ const Profile = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePassword = (currentPassword: string, newPassword: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setIsPasswordModalOpen(false)
|
||||||
|
axios
|
||||||
|
.patch(`/SASjsApi/auth/updatePassword`, {
|
||||||
|
currentPassword,
|
||||||
|
newPassword
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
toast.success('Password updated', {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed: ' + err.response?.data || err.text, {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<>
|
||||||
<CardHeader title="Profile Information" />
|
<Card>
|
||||||
<Divider />
|
<CardHeader title="Profile Information" />
|
||||||
<CardContent>
|
<Divider />
|
||||||
<Grid container spacing={4}>
|
<CardContent>
|
||||||
<Grid item md={6} xs={12}>
|
<Grid container spacing={4}>
|
||||||
<TextField
|
<Grid item md={6} xs={12}>
|
||||||
fullWidth
|
<TextField
|
||||||
error={user.displayName?.length === 0}
|
fullWidth
|
||||||
helperText="Please specify display name"
|
error={user.displayName?.length === 0}
|
||||||
label="Display Name"
|
helperText="Please specify display name"
|
||||||
name="displayName"
|
label="Display Name"
|
||||||
onChange={handleChange}
|
name="displayName"
|
||||||
required
|
onChange={handleChange}
|
||||||
value={user.displayName}
|
required
|
||||||
variant="outlined"
|
value={user.displayName}
|
||||||
disabled={appContext.mode === ModeType.Desktop}
|
variant="outlined"
|
||||||
/>
|
disabled={appContext.mode === ModeType.Desktop}
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item md={6} xs={12}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
error={user.username?.length === 0}
|
|
||||||
helperText="Please specify username"
|
|
||||||
label="Username"
|
|
||||||
name="username"
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
value={user.username}
|
|
||||||
variant="outlined"
|
|
||||||
disabled={appContext.mode === ModeType.Desktop}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item lg={6} md={8} sm={12} xs={12}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="autoExec"
|
|
||||||
name="autoExec"
|
|
||||||
onChange={handleChange}
|
|
||||||
multiline
|
|
||||||
rows="10"
|
|
||||||
value={user.autoExec}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormGroup row>
|
|
||||||
<FormControlLabel
|
|
||||||
disabled
|
|
||||||
control={<Checkbox checked={user.isActive} />}
|
|
||||||
label="isActive"
|
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
</Grid>
|
||||||
disabled
|
|
||||||
control={<Checkbox checked={user.isAdmin} />}
|
<Grid item md={6} xs={12}>
|
||||||
label="isAdmin"
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
error={user.username?.length === 0}
|
||||||
|
helperText="Please specify username"
|
||||||
|
label="Username"
|
||||||
|
name="username"
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
value={user.username}
|
||||||
|
variant="outlined"
|
||||||
|
disabled={appContext.mode === ModeType.Desktop}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item lg={6} md={8} sm={12} xs={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="autoExec"
|
||||||
|
name="autoExec"
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
rows="10"
|
||||||
|
value={user.autoExec}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormGroup row>
|
||||||
|
<FormControlLabel
|
||||||
|
disabled
|
||||||
|
control={<Checkbox checked={user.isActive} />}
|
||||||
|
label="isActive"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
disabled
|
||||||
|
control={<Checkbox checked={user.isAdmin} />}
|
||||||
|
label="isAdmin"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setIsPasswordModalOpen(true)}
|
||||||
|
>
|
||||||
|
Update Password
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</CardContent>
|
||||||
</CardContent>
|
<Divider />
|
||||||
<Divider />
|
<CardActions>
|
||||||
<CardActions>
|
<Button type="submit" variant="contained" onClick={handleSubmit}>
|
||||||
<Button type="submit" variant="contained" onClick={handleSubmit}>
|
Save Changes
|
||||||
Save Changes
|
</Button>
|
||||||
</Button>
|
</CardActions>
|
||||||
</CardActions>
|
</Card>
|
||||||
</Card>
|
<UpdatePasswordModal
|
||||||
|
open={isPasswordModalOpen}
|
||||||
|
setOpen={setIsPasswordModalOpen}
|
||||||
|
title="Update Password"
|
||||||
|
updatePassword={updatePassword}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const SASjsEditor = ({
|
|||||||
setTab
|
setTab
|
||||||
}: SASjsEditorProps) => {
|
}: SASjsEditorProps) => {
|
||||||
const {
|
const {
|
||||||
ctrlPressed,
|
|
||||||
fileContent,
|
fileContent,
|
||||||
isLoading,
|
isLoading,
|
||||||
log,
|
log,
|
||||||
@@ -64,8 +63,6 @@ const SASjsEditor = ({
|
|||||||
handleDiffEditorDidMount,
|
handleDiffEditorDidMount,
|
||||||
handleEditorDidMount,
|
handleEditorDidMount,
|
||||||
handleFilePathInput,
|
handleFilePathInput,
|
||||||
handleKeyDown,
|
|
||||||
handleKeyUp,
|
|
||||||
handleRunBtnClick,
|
handleRunBtnClick,
|
||||||
handleTabChange,
|
handleTabChange,
|
||||||
saveFile,
|
saveFile,
|
||||||
@@ -99,7 +96,6 @@ const SASjsEditor = ({
|
|||||||
original={prevFileContent}
|
original={prevFileContent}
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
editorDidMount={handleDiffEditorDidMount}
|
editorDidMount={handleDiffEditorDidMount}
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
onChange={(val) => setFileContent(val)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -108,7 +104,6 @@ const SASjsEditor = ({
|
|||||||
language={getLanguageFromExtension(selectedFileExtension)}
|
language={getLanguageFromExtension(selectedFileExtension)}
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
editorDidMount={handleEditorDidMount}
|
editorDidMount={handleEditorDidMount}
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
onChange={(val) => setFileContent(val)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -176,8 +171,6 @@ const SASjsEditor = ({
|
|||||||
{fileMenu}
|
{fileMenu}
|
||||||
</Box>
|
</Box>
|
||||||
<Paper
|
<Paper
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
sx={{
|
sx={{
|
||||||
height: 'calc(100vh - 170px)',
|
height: 'calc(100vh - 170px)',
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
@@ -204,7 +197,10 @@ const SASjsEditor = ({
|
|||||||
<StyledTabPanel value="log">
|
<StyledTabPanel value="log">
|
||||||
<div>
|
<div>
|
||||||
<h2>Log</h2>
|
<h2>Log</h2>
|
||||||
<pre id="log" style={{ overflow: 'auto', height: '75vh' }}>
|
<pre
|
||||||
|
id="log"
|
||||||
|
style={{ overflow: 'auto', height: 'calc(100vh - 220px)' }}
|
||||||
|
>
|
||||||
{log}
|
{log}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const useEditor = ({
|
|||||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||||
const [fileContent, setFileContent] = useState('')
|
const [fileContent, setFileContent] = useState('')
|
||||||
const [log, setLog] = useState('')
|
const [log, setLog] = useState('')
|
||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
|
||||||
const [webout, setWebout] = useState('')
|
const [webout, setWebout] = useState('')
|
||||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
@@ -50,7 +49,7 @@ const useEditor = ({
|
|||||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||||
const [showDiff, setShowDiff] = useState(false)
|
const [showDiff, setShowDiff] = useState(false)
|
||||||
|
|
||||||
const editorRef = useRef(null as any)
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
|
||||||
|
|
||||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||||
editorRef.current = editor
|
editorRef.current = editor
|
||||||
@@ -148,53 +147,47 @@ const useEditor = ({
|
|||||||
const handleRunBtnClick = () =>
|
const handleRunBtnClick = () =>
|
||||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||||
|
|
||||||
const runCode = (code: string) => {
|
const runCode = useCallback(
|
||||||
setIsLoading(true)
|
(code: string) => {
|
||||||
axios
|
setIsLoading(true)
|
||||||
.post(`/SASjsApi/code/execute`, {
|
axios
|
||||||
code: programPathInjection(
|
.post(`/SASjsApi/code/execute`, {
|
||||||
code,
|
code: programPathInjection(
|
||||||
selectedFilePath,
|
code,
|
||||||
selectedRunTime as RunTimeType
|
selectedFilePath,
|
||||||
),
|
selectedRunTime as RunTimeType
|
||||||
runTime: selectedRunTime
|
),
|
||||||
})
|
runTime: selectedRunTime
|
||||||
.then((res: any) => {
|
})
|
||||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
.then((res: any) => {
|
||||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||||
setTab('log')
|
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||||
|
setTab('log')
|
||||||
|
|
||||||
// Scroll to bottom of log
|
// Scroll to bottom of log
|
||||||
const logElement = document.getElementById('log')
|
const logElement = document.getElementById('log')
|
||||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setModalTitle('Abort')
|
setModalTitle('Abort')
|
||||||
setModalPayload(
|
setModalPayload(
|
||||||
typeof err.response.data === 'object'
|
typeof err.response.data === 'object'
|
||||||
? JSON.stringify(err.response.data)
|
? JSON.stringify(err.response.data)
|
||||||
: err.response.data
|
: err.response.data
|
||||||
)
|
)
|
||||||
setOpenModal(true)
|
setOpenModal(true)
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false))
|
.finally(() => setIsLoading(false))
|
||||||
}
|
},
|
||||||
|
[
|
||||||
const handleKeyDown = (event: any) => {
|
selectedFilePath,
|
||||||
if (event.ctrlKey) {
|
selectedRunTime,
|
||||||
if (event.key === 'v') {
|
setModalPayload,
|
||||||
setCtrlPressed(false)
|
setModalTitle,
|
||||||
}
|
setOpenModal,
|
||||||
|
setTab
|
||||||
if (event.key === 'Enter')
|
]
|
||||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
)
|
||||||
if (!ctrlPressed) setCtrlPressed(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUp = (event: any) => {
|
|
||||||
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||||
setSelectedRunTime(event.target.value as RunTimeType)
|
setSelectedRunTime(event.target.value as RunTimeType)
|
||||||
@@ -206,7 +199,7 @@ const useEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editorRef.current.addAction({
|
const saveFileAction = editorRef.current?.addAction({
|
||||||
// An unique identifier of the contributed action.
|
// An unique identifier of the contributed action.
|
||||||
id: 'save-file',
|
id: 'save-file',
|
||||||
|
|
||||||
@@ -216,6 +209,8 @@ const useEditor = ({
|
|||||||
// An optional array of keybindings for the action.
|
// An optional array of keybindings for the action.
|
||||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||||
|
|
||||||
|
contextMenuGroupId: '9_cutcopypaste',
|
||||||
|
|
||||||
// Method that will be executed when the action is triggered.
|
// Method that will be executed when the action is triggered.
|
||||||
// @param editor The editor instance is passed in as a convenience
|
// @param editor The editor instance is passed in as a convenience
|
||||||
run: () => {
|
run: () => {
|
||||||
@@ -223,7 +218,31 @@ const useEditor = ({
|
|||||||
if (prevFileContent !== fileContent) return saveFile()
|
if (prevFileContent !== fileContent) return saveFile()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
|
|
||||||
|
const runCodeAction = editorRef.current?.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'run-code',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Run Code',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
||||||
|
|
||||||
|
contextMenuGroupId: 'navigation',
|
||||||
|
|
||||||
|
// Method that will be executed when the action is triggered.
|
||||||
|
// @param editor The editor instance is passed in as a convenience
|
||||||
|
run: function () {
|
||||||
|
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
saveFileAction?.dispose()
|
||||||
|
runCodeAction?.dispose()
|
||||||
|
}
|
||||||
|
}, [fileContent, prevFileContent, selectedFilePath, saveFile, runCode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
@@ -242,8 +261,10 @@ const useEditor = ({
|
|||||||
axios
|
axios
|
||||||
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
|
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
setPrevFileContent(res.data)
|
const content =
|
||||||
setFileContent(res.data)
|
typeof res.data === 'object' ? JSON.stringify(res.data) : res.data
|
||||||
|
setPrevFileContent(content)
|
||||||
|
setFileContent(content)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setModalTitle('Abort')
|
setModalTitle('Abort')
|
||||||
@@ -277,7 +298,6 @@ const useEditor = ({
|
|||||||
}, [selectedFileExtension, runTimes])
|
}, [selectedFileExtension, runTimes])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ctrlPressed,
|
|
||||||
fileContent,
|
fileContent,
|
||||||
isLoading,
|
isLoading,
|
||||||
log,
|
log,
|
||||||
@@ -293,8 +313,6 @@ const useEditor = ({
|
|||||||
handleDiffEditorDidMount,
|
handleDiffEditorDidMount,
|
||||||
handleEditorDidMount,
|
handleEditorDidMount,
|
||||||
handleFilePathInput,
|
handleFilePathInput,
|
||||||
handleKeyDown,
|
|
||||||
handleKeyUp,
|
|
||||||
handleRunBtnClick,
|
handleRunBtnClick,
|
||||||
handleTabChange,
|
handleTabChange,
|
||||||
saveFile,
|
saveFile,
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ const SideBar = ({
|
|||||||
{directoryData && (
|
{directoryData && (
|
||||||
<TreeView
|
<TreeView
|
||||||
node={directoryData}
|
node={directoryData}
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
handleSelect={handleFileSelect}
|
handleSelect={handleFileSelect}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
addFile={addFile}
|
addFile={addFile}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ interface AppContextProps {
|
|||||||
checkingSession: boolean
|
checkingSession: boolean
|
||||||
loggedIn: boolean
|
loggedIn: boolean
|
||||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
|
setNeedsToUpdatePassword: Dispatch<SetStateAction<boolean>> | null
|
||||||
userId: number
|
userId: number
|
||||||
setUserId: Dispatch<SetStateAction<number>> | null
|
setUserId: Dispatch<SetStateAction<number>> | null
|
||||||
username: string
|
username: string
|
||||||
@@ -42,6 +44,8 @@ export const AppContext = createContext<AppContextProps>({
|
|||||||
checkingSession: false,
|
checkingSession: false,
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
setLoggedIn: null,
|
setLoggedIn: null,
|
||||||
|
needsToUpdatePassword: false,
|
||||||
|
setNeedsToUpdatePassword: null,
|
||||||
userId: 0,
|
userId: 0,
|
||||||
setUserId: null,
|
setUserId: null,
|
||||||
username: '',
|
username: '',
|
||||||
@@ -59,6 +63,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
const { children } = props
|
const { children } = props
|
||||||
const [checkingSession, setCheckingSession] = useState(false)
|
const [checkingSession, setCheckingSession] = useState(false)
|
||||||
const [loggedIn, setLoggedIn] = useState(false)
|
const [loggedIn, setLoggedIn] = useState(false)
|
||||||
|
const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false)
|
||||||
const [userId, setUserId] = useState(0)
|
const [userId, setUserId] = useState(0)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState('')
|
||||||
@@ -79,6 +84,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
setDisplayName(data.displayName)
|
setDisplayName(data.displayName)
|
||||||
setIsAdmin(data.isAdmin)
|
setIsAdmin(data.isAdmin)
|
||||||
setLoggedIn(true)
|
setLoggedIn(true)
|
||||||
|
setNeedsToUpdatePassword(data.needsToUpdatePassword)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoggedIn(false)
|
setLoggedIn(false)
|
||||||
@@ -120,6 +126,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
checkingSession,
|
checkingSession,
|
||||||
loggedIn,
|
loggedIn,
|
||||||
setLoggedIn,
|
setLoggedIn,
|
||||||
|
needsToUpdatePassword,
|
||||||
|
setNeedsToUpdatePassword,
|
||||||
userId,
|
userId,
|
||||||
setUserId,
|
setUserId,
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ code {
|
|||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.container {
|
||||||
margin: 50px 10px 0 10px;
|
margin: 50px 10px 0 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -25,15 +25,3 @@ code {
|
|||||||
padding: '5px 10px';
|
padding: '5px 10px';
|
||||||
margin-top: '10px';
|
margin-top: '10px';
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-item-label {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-label.selected {
|
|
||||||
background: lightgoldenrodyellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-label:hover {
|
|
||||||
background: lightgray;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user