mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb6a556630 | ||
|
|
9dbd8e16bd | ||
| fe07c41f5f | |||
| acc25cbd68 | |||
| 4ca61feda6 | |||
| abd5c64b4a | |||
| 2413c05fea | |||
|
|
4c874c2c39 | ||
|
|
d819d79bc9 | ||
| c51b50428f | |||
|
|
e10a0554f0 | ||
|
|
337e2eb2a0 | ||
| 66f8e7840b | |||
| 1c9d167f86 | |||
|
|
7e684b54a6 | ||
|
|
aafda2922b | ||
| 418bf41e38 | |||
| 81f0b03b09 | |||
| fe5ae44aab | |||
| 36be3a7d5e | |||
| 6434123401 | |||
|
|
0a6b972c65 | ||
|
|
be11707042 | ||
| 2412622367 | |||
|
|
de3a190a8d | ||
|
|
d5daafc6ed | ||
|
|
b1a2677b8c | ||
|
|
94072c3d24 | ||
|
|
b64c0c12da | ||
|
|
79bc7b0e28 | ||
|
|
fda0e0b57d | ||
|
|
14731e8824 | ||
|
|
258cc35f14 | ||
|
|
2295a518f0 | ||
|
|
1e5d621817 | ||
| 4d64420c45 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,8 +5,6 @@ node_modules/
|
||||
.env*
|
||||
sas/
|
||||
sasjs_root/
|
||||
api/mocks/custom/*
|
||||
!api/mocks/custom/.keep
|
||||
tmp/
|
||||
build/
|
||||
sasjsbuild/
|
||||
|
||||
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,3 +1,68 @@
|
||||
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* make access token duration configurable when creating client/secret ([2413c05](https://github.com/sasjs/server/commit/2413c05fea3960f7e5c3c8b7b2f85d61314f08db))
|
||||
* make refresh token duration configurable ([abd5c64](https://github.com/sasjs/server/commit/abd5c64b4a726e3f17594a98111b6aa269b71fee))
|
||||
|
||||
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
|
||||
|
||||
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
|
||||
|
||||
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
|
||||
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
|
||||
|
||||
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
|
||||
|
||||
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added domain for session cookies ([94072c3](https://github.com/sasjs/server/commit/94072c3d24a4d0d4c97900dc31bfbf1c9d2559b7))
|
||||
|
||||
## [0.23.2](https://github.com/sasjs/server/compare/v0.23.1...v0.23.2) (2022-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump in correct place ([14731e8](https://github.com/sasjs/server/commit/14731e8824fa9f3d1daf89fd62f9916d5e3fcae4))
|
||||
* bumping sasjs/score ([258cc35](https://github.com/sasjs/server/commit/258cc35f14cf50f2160f607000c60de27593fd79))
|
||||
* reverting commit ([fda0e0b](https://github.com/sasjs/server/commit/fda0e0b57d56e3b5231e626a8d933343ac0c5cdc))
|
||||
|
||||
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ldap issues ([4d64420](https://github.com/sasjs/server/commit/4d64420c45424134b4d2014a2d5dd6e846ed03b3))
|
||||
|
||||
# [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@ R_PATH=/usr/bin/Rscript
|
||||
SASJS_ROOT=./sasjs_root
|
||||
|
||||
|
||||
# This location is for files, sasjs packages and appStreamConfig.json
|
||||
DRIVE_LOCATION=./sasjs_root/drive
|
||||
|
||||
|
||||
# options: [http|https] default: http
|
||||
PROTOCOL=
|
||||
|
||||
@@ -103,6 +107,11 @@ PORT=
|
||||
# If not present, mocking function is disabled
|
||||
MOCK_SERVERTYPE=
|
||||
|
||||
# default: /api/mocks
|
||||
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
|
||||
# Server will automatically use subdirectory accordingly
|
||||
STATIC_MOCK_LOCATION=
|
||||
|
||||
#
|
||||
## Additional SAS Options
|
||||
#
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
||||
ALLOWED_DOMAIN=<just domain e.g. example.com >
|
||||
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
@@ -14,7 +15,7 @@ HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
AUTH_PROVIDERS=[ldap|internal] default considered as internal
|
||||
AUTH_PROVIDERS=[ldap]
|
||||
|
||||
LDAP_URL= <LDAP_SERVER_URL>
|
||||
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
|
||||
@@ -29,6 +30,7 @@ PYTHON_PATH=/usr/bin/python
|
||||
R_PATH=/usr/bin/Rscript
|
||||
|
||||
SASJS_ROOT=./sasjs_root
|
||||
DRIVE_LOCATION=./sasjs_root/drive
|
||||
|
||||
LOG_FORMAT_MORGAN=common
|
||||
LOG_LOCATION=./sasjs_root/logs
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="content">
|
||||
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
||||
<!--form container-->
|
||||
<input type="hidden" name="lt" value="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="_eventId" value="submit" aria-hidden="true" />
|
||||
|
||||
26
api/package-lock.json
generated
26
api/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "api",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.31.3",
|
||||
"@sasjs/core": "^4.40.1",
|
||||
"@sasjs/utils": "2.48.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
@@ -1394,9 +1394,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.31.3.tgz",
|
||||
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.40.1.tgz",
|
||||
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "2.48.1",
|
||||
@@ -7092,9 +7092,9 @@
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -11135,9 +11135,9 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/core": {
|
||||
"version": "4.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.31.3.tgz",
|
||||
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.40.1.tgz",
|
||||
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.48.1",
|
||||
@@ -15592,9 +15592,9 @@
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.31.3",
|
||||
"@sasjs/core": "^4.40.1",
|
||||
"@sasjs/utils": "2.48.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
|
||||
@@ -57,6 +57,16 @@ components:
|
||||
type: string
|
||||
description: 'Client Secret'
|
||||
example: someRandomCryptoString
|
||||
accessTokenExpiryDays:
|
||||
type: number
|
||||
format: double
|
||||
description: 'Number of days in which access token will expire'
|
||||
example: 1
|
||||
refreshTokenExpiryDays:
|
||||
type: number
|
||||
format: double
|
||||
description: 'Number of days in which access token will expire'
|
||||
example: 30
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
@@ -679,8 +689,8 @@ paths:
|
||||
$ref: '#/components/schemas/ClientPayload'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
|
||||
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
|
||||
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiryDays: 1, refreshTokenExpiryDays: 30}
|
||||
summary: "Admin only task. Create client with the following attributes:\nClientId,\nClientSecret,\naccessTokenExpiryDays (optional),\nrefreshTokenExpiryDays (optional)"
|
||||
tags:
|
||||
- Client
|
||||
security:
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Express } from 'express'
|
||||
import { Express, CookieOptions } from 'express'
|
||||
import mongoose from 'mongoose'
|
||||
import session from 'express-session'
|
||||
import MongoStore from 'connect-mongo'
|
||||
|
||||
import { ModeType } from '../utils'
|
||||
import { cookieOptions } from '../app'
|
||||
import { ModeType, ProtocolType } from '../utils'
|
||||
|
||||
export const configureExpressSession = (app: Express) => {
|
||||
const { MODE } = process.env
|
||||
@@ -19,6 +18,15 @@ export const configureExpressSession = (app: Express) => {
|
||||
})
|
||||
}
|
||||
|
||||
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
|
||||
const cookieOptions: CookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
domain: ALLOWED_DOMAIN?.trim() || undefined
|
||||
}
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.secrets.SESSION_SECRET,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler, CookieOptions } from 'express'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
getWebBuildFolder,
|
||||
instantiateLogger,
|
||||
loadAppStreamConfig,
|
||||
ProtocolType,
|
||||
ReturnCode,
|
||||
setProcessVariables,
|
||||
setupFolders,
|
||||
setupUserAutoExec,
|
||||
verifyEnvVariables
|
||||
} from './utils'
|
||||
import {
|
||||
@@ -29,15 +29,6 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
||||
|
||||
const app = express()
|
||||
|
||||
const { PROTOCOL } = process.env
|
||||
|
||||
export const cookieOptions: CookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
console.error(err.stack)
|
||||
res.status(500).send('Something broke!')
|
||||
@@ -72,8 +63,12 @@ export default setProcessVariables().then(async () => {
|
||||
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
await setupFolders()
|
||||
await copySASjsCore()
|
||||
await setupUserAutoExec()
|
||||
|
||||
if (process.driveLoc === path.join(process.sasjsRoot, 'drive')) {
|
||||
await setupFolders()
|
||||
await copySASjsCore()
|
||||
}
|
||||
|
||||
// loading these modules after setting up variables due to
|
||||
// multer's usage of process var process.driveLoc
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
removeTokensInDB,
|
||||
saveTokensInDB
|
||||
} from '../utils'
|
||||
import Client from '../model/Client'
|
||||
|
||||
@Route('SASjsApi/auth')
|
||||
@Tags('Auth')
|
||||
@@ -83,8 +84,17 @@ const token = async (data: any): Promise<TokenResponse> => {
|
||||
}
|
||||
}
|
||||
|
||||
const accessToken = generateAccessToken(userInfo)
|
||||
const refreshToken = generateRefreshToken(userInfo)
|
||||
const client = await Client.findOne({ clientId })
|
||||
if (!client) throw new Error('Invalid clientId.')
|
||||
|
||||
const accessToken = generateAccessToken(
|
||||
userInfo,
|
||||
client.accessTokenExpiryDays
|
||||
)
|
||||
const refreshToken = generateRefreshToken(
|
||||
userInfo,
|
||||
client.refreshTokenExpiryDays
|
||||
)
|
||||
|
||||
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
||||
|
||||
@@ -92,8 +102,17 @@ const token = async (data: any): Promise<TokenResponse> => {
|
||||
}
|
||||
|
||||
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
||||
const accessToken = generateAccessToken(userInfo)
|
||||
const refreshToken = generateRefreshToken(userInfo)
|
||||
const client = await Client.findOne({ clientId: userInfo.clientId })
|
||||
if (!client) throw new Error('Invalid clientId.')
|
||||
|
||||
const accessToken = generateAccessToken(
|
||||
userInfo,
|
||||
client.accessTokenExpiryDays
|
||||
)
|
||||
const refreshToken = generateRefreshToken(
|
||||
userInfo,
|
||||
client.refreshTokenExpiryDays
|
||||
)
|
||||
|
||||
await saveTokensInDB(
|
||||
userInfo.userId,
|
||||
|
||||
@@ -7,12 +7,18 @@ import Client, { ClientPayload } from '../model/Client'
|
||||
@Tags('Client')
|
||||
export class ClientController {
|
||||
/**
|
||||
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
|
||||
* @summary Admin only task. Create client with the following attributes:
|
||||
* ClientId,
|
||||
* ClientSecret,
|
||||
* accessTokenExpiryDays (optional),
|
||||
* refreshTokenExpiryDays (optional)
|
||||
*
|
||||
*/
|
||||
@Example<ClientPayload>({
|
||||
clientId: 'someFormattedClientID1234',
|
||||
clientSecret: 'someRandomCryptoString'
|
||||
clientSecret: 'someRandomCryptoString',
|
||||
accessTokenExpiryDays: 1,
|
||||
refreshTokenExpiryDays: 30
|
||||
})
|
||||
@Post('/')
|
||||
public async createClient(
|
||||
@@ -22,8 +28,13 @@ export class ClientController {
|
||||
}
|
||||
}
|
||||
|
||||
const createClient = async (data: any): Promise<ClientPayload> => {
|
||||
const { clientId, clientSecret } = data
|
||||
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
||||
const {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessTokenExpiryDays,
|
||||
refreshTokenExpiryDays
|
||||
} = data
|
||||
|
||||
// Checking if client is already in the database
|
||||
const clientExist = await Client.findOne({ clientId })
|
||||
@@ -32,13 +43,16 @@ const createClient = async (data: any): Promise<ClientPayload> => {
|
||||
// Create a new client
|
||||
const client = new Client({
|
||||
clientId,
|
||||
clientSecret
|
||||
clientSecret,
|
||||
accessTokenExpiryDays
|
||||
})
|
||||
|
||||
const savedClient = await client.save()
|
||||
|
||||
return {
|
||||
clientId: savedClient.clientId,
|
||||
clientSecret: savedClient.clientSecret
|
||||
clientSecret: savedClient.clientSecret,
|
||||
accessTokenExpiryDays: savedClient.accessTokenExpiryDays,
|
||||
refreshTokenExpiryDays: savedClient.refreshTokenExpiryDays
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ const updateUsersListInGroup = async (
|
||||
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||
}
|
||||
|
||||
if (group.authProvider !== AuthProviderType.Internal)
|
||||
if (group.authProvider)
|
||||
throw {
|
||||
code: 405,
|
||||
status: 'Method Not Allowed',
|
||||
@@ -266,7 +266,7 @@ const updateUsersListInGroup = async (
|
||||
message: 'User not found.'
|
||||
}
|
||||
|
||||
if (user.authProvider !== AuthProviderType.Internal)
|
||||
if (user.authProvider)
|
||||
throw {
|
||||
code: 405,
|
||||
status: 'Method Not Allowed',
|
||||
|
||||
@@ -28,6 +28,7 @@ interface ExecuteFileParams {
|
||||
returnJson?: boolean
|
||||
session?: Session
|
||||
runTime: RunTimeType
|
||||
forceStringResult?: boolean
|
||||
}
|
||||
|
||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||
@@ -42,7 +43,8 @@ export class ExecutionController {
|
||||
otherArgs,
|
||||
returnJson,
|
||||
session,
|
||||
runTime
|
||||
runTime,
|
||||
forceStringResult
|
||||
}: ExecuteFileParams) {
|
||||
const program = await readFile(programPath)
|
||||
|
||||
@@ -53,7 +55,8 @@ export class ExecutionController {
|
||||
otherArgs,
|
||||
returnJson,
|
||||
session,
|
||||
runTime
|
||||
runTime,
|
||||
forceStringResult
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +66,8 @@ export class ExecutionController {
|
||||
vars,
|
||||
otherArgs,
|
||||
session: sessionByFileUpload,
|
||||
runTime
|
||||
runTime,
|
||||
forceStringResult
|
||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
@@ -104,7 +108,7 @@ export class ExecutionController {
|
||||
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? fileResponse
|
||||
? fileResponse && !forceStringResult
|
||||
? await readFileBinary(weboutPath)
|
||||
: await readFile(weboutPath)
|
||||
: ''
|
||||
|
||||
@@ -110,17 +110,13 @@ export const processProgram = async (
|
||||
|
||||
// create a stream that will write to console outputs to log file
|
||||
const writeStream = fs.createWriteStream(logPath)
|
||||
|
||||
// waiting for the open event so that we can have underlying file descriptor
|
||||
await once(writeStream, 'open')
|
||||
|
||||
execFileSync(executablePath, [codePath], {
|
||||
stdio: ['ignore', writeStream, writeStream]
|
||||
})
|
||||
|
||||
// copy the code file to log and end write stream
|
||||
writeStream.end(program)
|
||||
|
||||
session.completed = true
|
||||
console.log('session completed', session)
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -2,6 +2,16 @@ import { readFile } from '@sasjs/utils'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import { Request, Post, Get } from 'tsoa'
|
||||
import dotenv from 'dotenv'
|
||||
import { ExecutionController } from './internal'
|
||||
import {
|
||||
getPreProgramVariables,
|
||||
getRunTimeAndFilePath,
|
||||
makeFilesNamesMap
|
||||
} from '../utils'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export interface Sas9Response {
|
||||
content: string
|
||||
@@ -16,9 +26,17 @@ export interface MockFileRead {
|
||||
|
||||
export class MockSas9Controller {
|
||||
private loggedIn: string | undefined
|
||||
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||
|
||||
@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) {
|
||||
return {
|
||||
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([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'generic',
|
||||
'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) {
|
||||
console.log('err', err)
|
||||
}
|
||||
|
||||
return {
|
||||
content: 'No webout returned.'
|
||||
}
|
||||
}
|
||||
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
...filePath
|
||||
])
|
||||
}
|
||||
|
||||
@Post('/SASStoredProcess/do/')
|
||||
public async sasStoredProcessDo(
|
||||
public async sasStoredProcessDoPost(
|
||||
@Request() req: express.Request
|
||||
): Promise<Sas9Response> {
|
||||
if (!this.loggedIn) {
|
||||
@@ -53,23 +141,38 @@ export class MockSas9Controller {
|
||||
}
|
||||
}
|
||||
|
||||
let program = req.query._program?.toString() || ''
|
||||
program = program.replace('/', '')
|
||||
const program = req.query._program ?? req.body?._program
|
||||
const vars = {
|
||||
...req.query,
|
||||
...req.body,
|
||||
_requestMethod: req.method,
|
||||
_driveLoc: process.driveLoc
|
||||
}
|
||||
const filesNamesMap = req.files?.length
|
||||
? makeFilesNamesMap(req.files as MulterFile[])
|
||||
: null
|
||||
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
|
||||
try {
|
||||
const result = await new ExecutionController().executeFile({
|
||||
programPath: codePath,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: vars,
|
||||
otherArgs: otherArgs,
|
||||
runTime,
|
||||
session: req.sasjsSession,
|
||||
forceStringResult: true
|
||||
})
|
||||
|
||||
const content = await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
...program.split('/')
|
||||
])
|
||||
|
||||
if (content.error) {
|
||||
return content
|
||||
return {
|
||||
content: result.result as string
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err', err)
|
||||
}
|
||||
|
||||
const parsedContent = parseJsonIfValid(content.content)
|
||||
|
||||
return {
|
||||
content: parsedContent
|
||||
content: 'No webout returned.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +188,8 @@ export class MockSas9Controller {
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'generic',
|
||||
'sas9',
|
||||
'generic',
|
||||
'logged-in'
|
||||
])
|
||||
}
|
||||
@@ -95,21 +198,27 @@ export class MockSas9Controller {
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'generic',
|
||||
'sas9',
|
||||
'generic',
|
||||
'login'
|
||||
])
|
||||
}
|
||||
|
||||
@Post('/SASLogon/login')
|
||||
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||
if (req.body.lt && req.body.lt !== 'validtoken')
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASLogon/login'
|
||||
}
|
||||
|
||||
this.loggedIn = req.body.username
|
||||
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'generic',
|
||||
'sas9',
|
||||
'generic',
|
||||
'logged-in'
|
||||
])
|
||||
}
|
||||
@@ -122,8 +231,8 @@ export class MockSas9Controller {
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'generic',
|
||||
'sas9',
|
||||
'generic',
|
||||
'public-access-denied'
|
||||
])
|
||||
}
|
||||
@@ -131,8 +240,8 @@ export class MockSas9Controller {
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'generic',
|
||||
'sas9',
|
||||
'generic',
|
||||
'logged-out'
|
||||
])
|
||||
}
|
||||
@@ -152,23 +261,6 @@ export class MockSas9Controller {
|
||||
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 (
|
||||
filePath: string[]
|
||||
): Promise<MockFileRead> => {
|
||||
|
||||
@@ -299,14 +299,19 @@ const updateUser = async (
|
||||
|
||||
const user = await User.findOne(findBy)
|
||||
|
||||
if (
|
||||
user?.authProvider !== AuthProviderType.Internal &&
|
||||
(username !== user?.username || displayName !== user?.displayName)
|
||||
) {
|
||||
if (username && username !== user?.username && user?.authProvider) {
|
||||
throw {
|
||||
code: 405,
|
||||
message:
|
||||
'Can not update username and display name of user that is created by an external auth provider.'
|
||||
'Can not update username of user that is created by an external auth provider.'
|
||||
}
|
||||
}
|
||||
|
||||
if (displayName && displayName !== user?.displayName && user?.authProvider) {
|
||||
throw {
|
||||
code: 405,
|
||||
message:
|
||||
'Can not update display name of user that is created by an external auth provider.'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ export interface ClientPayload {
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
clientSecret: string
|
||||
/**
|
||||
* Number of days in which access token will expire
|
||||
* @example 1
|
||||
*/
|
||||
accessTokenExpiryDays?: number
|
||||
/**
|
||||
* Number of days in which access token will expire
|
||||
* @example 30
|
||||
*/
|
||||
refreshTokenExpiryDays?: number
|
||||
}
|
||||
|
||||
const ClientSchema = new Schema<ClientPayload>({
|
||||
@@ -21,6 +31,14 @@ const ClientSchema = new Schema<ClientPayload>({
|
||||
clientSecret: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
accessTokenExpiryDays: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
refreshTokenExpiryDays: {
|
||||
type: Number,
|
||||
default: 30
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -50,8 +50,7 @@ const groupSchema = new Schema<IGroupDocument>({
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: AuthProviderType,
|
||||
default: 'internal'
|
||||
enum: AuthProviderType
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -71,8 +71,7 @@ const userSchema = new Schema<IUserDocument>({
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: AuthProviderType,
|
||||
default: 'internal'
|
||||
enum: AuthProviderType
|
||||
},
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
|
||||
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 webRouter from './web'
|
||||
import { MOCK_SERVERTYPEType } from '../../utils'
|
||||
import { csrfProtection } from '../../middlewares'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -18,7 +19,7 @@ switch (MOCK_SERVERTYPE) {
|
||||
break
|
||||
}
|
||||
default: {
|
||||
router.use('/', webRouter)
|
||||
router.use('/', csrfProtection, webRouter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,25 @@ import express from 'express'
|
||||
import { generateCSRFToken } from '../../middlewares'
|
||||
import { WebController } from '../../controllers'
|
||||
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
import { FileUploadController } from '../../controllers/internal'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const sas9WebRouter = express.Router()
|
||||
const webController = new WebController()
|
||||
// Mock controller must be singleton because it keeps the states
|
||||
// for example `isLoggedIn` and potentially more in future mocks
|
||||
const controller = new MockSas9Controller()
|
||||
const fileUploadController = new FileUploadController()
|
||||
|
||||
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||
|
||||
const upload = multer({
|
||||
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
|
||||
})
|
||||
|
||||
sas9WebRouter.get('/', async (req, res) => {
|
||||
let response
|
||||
@@ -27,7 +40,7 @@ sas9WebRouter.get('/', async (req, res) => {
|
||||
})
|
||||
|
||||
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||
const response = await controller.sasStoredProcess()
|
||||
const response = await controller.sasStoredProcess(req)
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
@@ -41,8 +54,8 @@ sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
|
||||
const response = await controller.sasStoredProcessDo(req)
|
||||
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
|
||||
const response = await controller.sasStoredProcessDoGet(req)
|
||||
|
||||
if (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) => {
|
||||
const response = await controller.loginGet()
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ webRouter.get('/', async (req, res) => {
|
||||
} catch (_) {
|
||||
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||
} finally {
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const { ALLOWED_DOMAIN } = process.env
|
||||
const allowedDomain = ALLOWED_DOMAIN?.trim()
|
||||
const domain = allowedDomain ? ` Domain=${allowedDomain};` : ''
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()};${domain} Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const injectedContent = response?.replace(
|
||||
'</head>',
|
||||
`${codeToInject}</head>`
|
||||
|
||||
1
api/src/types/system/process.d.ts
vendored
1
api/src/types/system/process.d.ts
vendored
@@ -5,6 +5,7 @@ declare namespace NodeJS {
|
||||
pythonLoc?: string
|
||||
rLoc?: string
|
||||
driveLoc: string
|
||||
sasjsRoot: string
|
||||
logsLoc: string
|
||||
logsUUID: string
|
||||
sessionController?: import('../../controllers/internal').SessionController
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||
export const copySASjsCore = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
console.log('Copying Macros from container to drive(tmp).')
|
||||
console.log('Copying Macros from container to drive.')
|
||||
|
||||
const macrosDrivePath = getMacrosFolder()
|
||||
|
||||
|
||||
@@ -20,22 +20,24 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
||||
export const getDesktopUserAutoExecPath = () =>
|
||||
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
||||
|
||||
export const getSasjsRootFolder = () => process.driveLoc
|
||||
export const getSasjsRootFolder = () => process.sasjsRoot
|
||||
|
||||
export const getSasjsDriveFolder = () => process.driveLoc
|
||||
|
||||
export const getLogFolder = () => process.logsLoc
|
||||
|
||||
export const getAppStreamConfigPath = () =>
|
||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
|
||||
|
||||
export const getMacrosFolder = () =>
|
||||
path.join(getSasjsRootFolder(), 'sas', 'sasautos')
|
||||
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
|
||||
|
||||
export const getPackagesFolder = () =>
|
||||
path.join(getSasjsRootFolder(), 'sas', 'sas_packages')
|
||||
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
|
||||
|
||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||
|
||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
||||
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
|
||||
|
||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { InfoJWT } from '../types'
|
||||
|
||||
export const generateAccessToken = (data: InfoJWT) =>
|
||||
export const generateAccessToken = (data: InfoJWT, expiry?: number) =>
|
||||
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
||||
expiresIn: '1day'
|
||||
expiresIn: expiry ? `${expiry}d` : '1d'
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { InfoJWT } from '../types'
|
||||
|
||||
export const generateRefreshToken = (data: InfoJWT) =>
|
||||
export const generateRefreshToken = (data: InfoJWT, expiry?: number) =>
|
||||
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
||||
expiresIn: '30 days'
|
||||
expiresIn: expiry ? `${expiry}d` : '30d'
|
||||
})
|
||||
|
||||
@@ -18,10 +18,12 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
||||
|
||||
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||
|
||||
//In desktop mode when mocking mode is enabled, user was undefined.
|
||||
//So this is workaround.
|
||||
return {
|
||||
username: user!.username,
|
||||
userId: user!.userId,
|
||||
displayName: user!.displayName,
|
||||
username: user ? user.username : 'demo',
|
||||
userId: user ? user.userId : 0,
|
||||
displayName: user ? user.displayName : 'demo',
|
||||
serverUrl: protocol + host,
|
||||
httpHeaders
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export * from './saveTokensInDB'
|
||||
export * from './seedDB'
|
||||
export * from './setProcessVariables'
|
||||
export * from './setupFolders'
|
||||
export * from './setupUserAutoExec'
|
||||
export * from './upload'
|
||||
export * from './validation'
|
||||
export * from './verifyEnvVariables'
|
||||
|
||||
@@ -19,7 +19,8 @@ export const setProcessVariables = async () => {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -32,7 +33,6 @@ export const setProcessVariables = async () => {
|
||||
process.rLoc = process.env.R_PATH
|
||||
} else {
|
||||
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
||||
|
||||
process.sasLoc = sasLoc
|
||||
process.nodeLoc = nodeLoc
|
||||
process.pythonLoc = pythonLoc
|
||||
@@ -42,11 +42,19 @@ export const setProcessVariables = async () => {
|
||||
const { SASJS_ROOT } = process.env
|
||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||
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 absLogsPath = getAbsolutePath(
|
||||
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
|
||||
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
|
||||
process.cwd()
|
||||
)
|
||||
await createFolder(absLogsPath)
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
||||
import {
|
||||
getDesktopUserAutoExecPath,
|
||||
getFilesFolder,
|
||||
getPackagesFolder
|
||||
} from './file'
|
||||
import { ModeType } from './verifyEnvVariables'
|
||||
import { createFolder } from '@sasjs/utils'
|
||||
import { getFilesFolder, getPackagesFolder } from './file'
|
||||
|
||||
export const setupFolders = async () => {
|
||||
const drivePath = getFilesFolder()
|
||||
await createFolder(drivePath)
|
||||
await createFolder(getFilesFolder())
|
||||
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(), '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,9 @@ export const updateUserValidation = (
|
||||
export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
clientId: Joi.string().required(),
|
||||
clientSecret: Joi.string().required()
|
||||
clientSecret: Joi.string().required(),
|
||||
accessTokenExpiryDays: Joi.number(),
|
||||
refreshTokenExpiryDays: Joi.number()
|
||||
}).validate(data)
|
||||
|
||||
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
@@ -9,8 +9,7 @@ export enum ModeType {
|
||||
}
|
||||
|
||||
export enum AuthProviderType {
|
||||
LDAP = 'ldap',
|
||||
Internal = 'internal'
|
||||
LDAP = 'ldap'
|
||||
}
|
||||
|
||||
export enum ProtocolType {
|
||||
@@ -111,7 +110,7 @@ const verifyMODE = (): string[] => {
|
||||
}
|
||||
|
||||
if (process.env.MODE === ModeType.Server) {
|
||||
const { DB_CONNECT, AUTH_MECHANISM } = process.env
|
||||
const { DB_CONNECT, AUTH_PROVIDERS } = process.env
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (!DB_CONNECT)
|
||||
@@ -119,14 +118,12 @@ const verifyMODE = (): string[] => {
|
||||
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
|
||||
)
|
||||
|
||||
if (AUTH_MECHANISM) {
|
||||
const authMechanismTypes = Object.values(AuthProviderType)
|
||||
if (!authMechanismTypes.includes(AUTH_MECHANISM as AuthProviderType))
|
||||
if (AUTH_PROVIDERS) {
|
||||
const authProvidersType = Object.values(AuthProviderType)
|
||||
if (!authProvidersType.includes(AUTH_PROVIDERS as AuthProviderType))
|
||||
errors.push(
|
||||
`- AUTH_MECHANISM '${AUTH_MECHANISM}'\n - valid options ${authMechanismTypes}`
|
||||
`- AUTH_PROVIDERS '${AUTH_PROVIDERS}'\n - valid options ${authProvidersType}`
|
||||
)
|
||||
} else {
|
||||
process.env.AUTH_MECHANISM = DEFAULTS.AUTH_MECHANISM
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,7 +267,7 @@ const verifyRUN_TIMES = (): string[] => {
|
||||
return errors
|
||||
}
|
||||
|
||||
const verifyExecutablePaths = () => {
|
||||
const verifyExecutablePaths = (): string[] => {
|
||||
const errors: string[] = []
|
||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
|
||||
process.env
|
||||
@@ -307,37 +304,37 @@ const verifyLDAPVariables = () => {
|
||||
LDAP_USERS_BASE_DN,
|
||||
LDAP_GROUPS_BASE_DN,
|
||||
MODE,
|
||||
AUTH_MECHANISM
|
||||
AUTH_PROVIDERS
|
||||
} = process.env
|
||||
|
||||
if (MODE === ModeType.Server && AUTH_MECHANISM === AuthProviderType.LDAP) {
|
||||
if (MODE === ModeType.Server && AUTH_PROVIDERS === AuthProviderType.LDAP) {
|
||||
if (!LDAP_URL) {
|
||||
errors.push(
|
||||
`- LDAP_URL is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
|
||||
`- LDAP_URL is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_BIND_DN) {
|
||||
errors.push(
|
||||
`- LDAP_BIND_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
|
||||
`- LDAP_BIND_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_BIND_PASSWORD) {
|
||||
errors.push(
|
||||
`- LDAP_BIND_PASSWORD is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
|
||||
`- LDAP_BIND_PASSWORD is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_USERS_BASE_DN) {
|
||||
errors.push(
|
||||
`- LDAP_USERS_BASE_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
|
||||
`- LDAP_USERS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_GROUPS_BASE_DN) {
|
||||
errors.push(
|
||||
`- LDAP_GROUPS_BASE_DN is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'`
|
||||
`- LDAP_GROUPS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -347,7 +344,6 @@ const verifyLDAPVariables = () => {
|
||||
|
||||
const DEFAULTS = {
|
||||
MODE: ModeType.Desktop,
|
||||
AUTH_MECHANISM: AuthProviderType.Internal,
|
||||
PROTOCOL: ProtocolType.HTTP,
|
||||
PORT: '5000',
|
||||
HELMET_COEP: HelmetCoepType.TRUE,
|
||||
|
||||
@@ -31,14 +31,24 @@ const DeleteConfirmationModal = ({
|
||||
message,
|
||||
_delete
|
||||
}: DeleteConfirmationModalProps) => {
|
||||
const handleDeleteClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
_delete()
|
||||
}
|
||||
|
||||
const handleClose = (event: any) => {
|
||||
event.stopPropagation()
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||
<BootstrapDialog onClose={handleClose} open={open}>
|
||||
<DialogContent dividers>
|
||||
<Typography gutterBottom>{message}</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<Button color="error" onClick={() => _delete()}>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button color="error" onClick={handleDeleteClick}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -69,8 +69,18 @@ const NameInputModal = ({
|
||||
action(name)
|
||||
}
|
||||
|
||||
const handleActionClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
action(name)
|
||||
}
|
||||
|
||||
const handleClose = (event: any) => {
|
||||
event.stopPropagation()
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||
<BootstrapDialog fullWidth onClose={handleClose} open={open}>
|
||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||
{title}
|
||||
</BootstrapDialogTitle>
|
||||
@@ -91,12 +101,12 @@ const NameInputModal = ({
|
||||
</form>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||
<Button variant="contained" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => action(name)}
|
||||
onClick={handleActionClick}
|
||||
disabled={hasError || !name}
|
||||
>
|
||||
{actionLabel}
|
||||
|
||||
@@ -1,67 +1,79 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Menu, MenuItem } from '@mui/material'
|
||||
import React, { useState } from 'react'
|
||||
import { Menu, MenuItem, Typography } from '@mui/material'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
import MuiTreeView from '@mui/lab/TreeView'
|
||||
import MuiTreeItem from '@mui/lab/TreeItem'
|
||||
|
||||
import DeleteConfirmationModal from './deleteConfirmationModal'
|
||||
import NameInputModal from './nameInputModal'
|
||||
|
||||
import { TreeNode } from '../utils/types'
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
node: TreeNode
|
||||
selectedFilePath: string
|
||||
handleSelect: (filePath: string) => void
|
||||
deleteNode: (path: string, isFolder: boolean) => void
|
||||
addFile: (path: string) => void
|
||||
addFolder: (path: string) => void
|
||||
rename: (oldPath: string, newPath: string) => void
|
||||
}
|
||||
|
||||
interface TreeViewProps extends Props {
|
||||
defaultExpanded?: string[]
|
||||
}
|
||||
|
||||
const TreeView = ({
|
||||
node,
|
||||
selectedFilePath,
|
||||
handleSelect,
|
||||
deleteNode,
|
||||
addFile,
|
||||
addFolder,
|
||||
rename,
|
||||
defaultExpanded
|
||||
}: Props) => {
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
listStyle: 'none',
|
||||
padding: '0.25rem 0.85rem',
|
||||
width: 'max-content'
|
||||
}}
|
||||
}: TreeViewProps) => {
|
||||
const renderTree = (nodes: TreeNode) => (
|
||||
<MuiTreeItem
|
||||
key={nodes.relativePath}
|
||||
nodeId={nodes.relativePath}
|
||||
label={
|
||||
<TreeItemWithContextMenu
|
||||
node={nodes}
|
||||
handleSelect={handleSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
rename={rename}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TreeViewNode
|
||||
node={node}
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
rename={rename}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
</ul>
|
||||
{Array.isArray(nodes.children)
|
||||
? nodes.children.map((node) => renderTree(node))
|
||||
: null}
|
||||
</MuiTreeItem>
|
||||
)
|
||||
|
||||
return (
|
||||
<MuiTreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
defaultExpanded={defaultExpanded}
|
||||
sx={{ flexGrow: 1, maxWidth: 400, overflowY: 'auto' }}
|
||||
>
|
||||
{renderTree(node)}
|
||||
</MuiTreeView>
|
||||
)
|
||||
}
|
||||
|
||||
export default TreeView
|
||||
|
||||
const TreeViewNode = ({
|
||||
const TreeItemWithContextMenu = ({
|
||||
node,
|
||||
selectedFilePath,
|
||||
handleSelect,
|
||||
deleteNode,
|
||||
addFile,
|
||||
addFolder,
|
||||
rename,
|
||||
defaultExpanded
|
||||
rename
|
||||
}: Props) => {
|
||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||
useState(false)
|
||||
@@ -72,18 +84,19 @@ const TreeViewNode = ({
|
||||
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
||||
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
||||
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||
const [childVisible, setChildVisibility] = useState(false)
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
mouseX: number
|
||||
mouseY: number
|
||||
} | null>(null)
|
||||
|
||||
const launchProgram = () => {
|
||||
const launchProgram = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
const baseUrl = window.location.origin
|
||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
||||
}
|
||||
|
||||
const launchProgramWithDebug = () => {
|
||||
const launchProgramWithDebug = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
const baseUrl = window.location.origin
|
||||
window.open(
|
||||
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
||||
@@ -103,25 +116,18 @@ const TreeViewNode = ({
|
||||
)
|
||||
}
|
||||
|
||||
const hasChild = node.children.length ? true : false
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (node.children.length) {
|
||||
setChildVisibility((v) => !v)
|
||||
return
|
||||
}
|
||||
const handleClose = (event: any) => {
|
||||
event.stopPropagation()
|
||||
setContextMenu(null)
|
||||
}
|
||||
|
||||
const handleItemClick = (event: React.MouseEvent) => {
|
||||
if (node.children.length) return
|
||||
handleSelect(node.relativePath)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
|
||||
setChildVisibility(true)
|
||||
defaultExpanded.shift()
|
||||
}
|
||||
}, [defaultExpanded, node.relativePath])
|
||||
|
||||
const handleDeleteItemClick = () => {
|
||||
const handleDeleteItemClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
setContextMenu(null)
|
||||
setDeleteConfirmationModalOpen(true)
|
||||
setDeleteConfirmationModalMessage(
|
||||
@@ -136,7 +142,8 @@ const TreeViewNode = ({
|
||||
deleteNode(node.relativePath, node.isFolder)
|
||||
}
|
||||
|
||||
const handleNewFolderItemClick = () => {
|
||||
const handleNewFolderItemClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
setContextMenu(null)
|
||||
setNameInputModalOpen(true)
|
||||
setNameInputModalTitle('Add Folder')
|
||||
@@ -145,7 +152,8 @@ const TreeViewNode = ({
|
||||
setDefaultInputModalName('')
|
||||
}
|
||||
|
||||
const handleNewFileItemClick = () => {
|
||||
const handleNewFileItemClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
setContextMenu(null)
|
||||
setNameInputModalOpen(true)
|
||||
setNameInputModalTitle('Add File')
|
||||
@@ -161,7 +169,8 @@ const TreeViewNode = ({
|
||||
else addFile(path)
|
||||
}
|
||||
|
||||
const handleRenameItemClick = () => {
|
||||
const handleRenameItemClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
setContextMenu(null)
|
||||
setNameInputModalOpen(true)
|
||||
setNameInputModalTitle('Rename')
|
||||
@@ -181,34 +190,7 @@ const TreeViewNode = ({
|
||||
|
||||
return (
|
||||
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||
<li style={{ display: 'list-item' }}>
|
||||
<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>
|
||||
<Typography onClick={handleItemClick}>{node.name}</Typography>
|
||||
<DeleteConfirmationModal
|
||||
open={deleteConfirmationModalOpen}
|
||||
setOpen={setDeleteConfirmationModalOpen}
|
||||
@@ -228,7 +210,7 @@ const TreeViewNode = ({
|
||||
/>
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={() => setContextMenu(null)}
|
||||
onClose={handleClose}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
contextMenu !== null
|
||||
|
||||
@@ -48,7 +48,6 @@ const SASjsEditor = ({
|
||||
setTab
|
||||
}: SASjsEditorProps) => {
|
||||
const {
|
||||
ctrlPressed,
|
||||
fileContent,
|
||||
isLoading,
|
||||
log,
|
||||
@@ -64,8 +63,6 @@ const SASjsEditor = ({
|
||||
handleDiffEditorDidMount,
|
||||
handleEditorDidMount,
|
||||
handleFilePathInput,
|
||||
handleKeyDown,
|
||||
handleKeyUp,
|
||||
handleRunBtnClick,
|
||||
handleTabChange,
|
||||
saveFile,
|
||||
@@ -99,7 +96,6 @@ const SASjsEditor = ({
|
||||
original={prevFileContent}
|
||||
value={fileContent}
|
||||
editorDidMount={handleDiffEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
) : (
|
||||
@@ -108,7 +104,6 @@ const SASjsEditor = ({
|
||||
language={getLanguageFromExtension(selectedFileExtension)}
|
||||
value={fileContent}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
)
|
||||
@@ -176,8 +171,6 @@ const SASjsEditor = ({
|
||||
{fileMenu}
|
||||
</Box>
|
||||
<Paper
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
sx={{
|
||||
height: 'calc(100vh - 170px)',
|
||||
padding: '10px',
|
||||
|
||||
@@ -42,7 +42,6 @@ const useEditor = ({
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||
const [webout, setWebout] = useState('')
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||
@@ -50,7 +49,7 @@ const useEditor = ({
|
||||
const [openFilePathInputModal, setOpenFilePathInputModal] = 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) => {
|
||||
editorRef.current = editor
|
||||
@@ -148,53 +147,47 @@ const useEditor = ({
|
||||
const handleRunBtnClick = () =>
|
||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||
|
||||
const runCode = (code: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, {
|
||||
code: programPathInjection(
|
||||
code,
|
||||
selectedFilePath,
|
||||
selectedRunTime as RunTimeType
|
||||
),
|
||||
runTime: selectedRunTime
|
||||
})
|
||||
.then((res: any) => {
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
setTab('log')
|
||||
const runCode = useCallback(
|
||||
(code: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, {
|
||||
code: programPathInjection(
|
||||
code,
|
||||
selectedFilePath,
|
||||
selectedRunTime as RunTimeType
|
||||
),
|
||||
runTime: selectedRunTime
|
||||
})
|
||||
.then((res: any) => {
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
setTab('log')
|
||||
|
||||
// Scroll to bottom of log
|
||||
const logElement = document.getElementById('log')
|
||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.ctrlKey) {
|
||||
if (event.key === 'v') {
|
||||
setCtrlPressed(false)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// Scroll to bottom of log
|
||||
const logElement = document.getElementById('log')
|
||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
},
|
||||
[
|
||||
selectedFilePath,
|
||||
selectedRunTime,
|
||||
setModalPayload,
|
||||
setModalTitle,
|
||||
setOpenModal,
|
||||
setTab
|
||||
]
|
||||
)
|
||||
|
||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||
setSelectedRunTime(event.target.value as RunTimeType)
|
||||
@@ -206,7 +199,7 @@ const useEditor = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current.addAction({
|
||||
const saveFileAction = editorRef.current?.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'save-file',
|
||||
|
||||
@@ -216,6 +209,8 @@ const useEditor = ({
|
||||
// An optional array of keybindings for the action.
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||
|
||||
contextMenuGroupId: '9_cutcopypaste',
|
||||
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: () => {
|
||||
@@ -223,7 +218,31 @@ const useEditor = ({
|
||||
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(() => {
|
||||
setRunTimes(Object.values(appContext.runTimes))
|
||||
@@ -277,7 +296,6 @@ const useEditor = ({
|
||||
}, [selectedFileExtension, runTimes])
|
||||
|
||||
return {
|
||||
ctrlPressed,
|
||||
fileContent,
|
||||
isLoading,
|
||||
log,
|
||||
@@ -293,8 +311,6 @@ const useEditor = ({
|
||||
handleDiffEditorDidMount,
|
||||
handleEditorDidMount,
|
||||
handleFilePathInput,
|
||||
handleKeyDown,
|
||||
handleKeyUp,
|
||||
handleRunBtnClick,
|
||||
handleTabChange,
|
||||
saveFile,
|
||||
|
||||
@@ -180,7 +180,6 @@ const SideBar = ({
|
||||
{directoryData && (
|
||||
<TreeView
|
||||
node={directoryData}
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleFileSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
|
||||
Reference in New Issue
Block a user