1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-11 19:44:35 +00:00

Compare commits

...

41 Commits

Author SHA1 Message Date
semantic-release-bot
ab5858b8af chore(release): 0.26.1 [skip ci]
## [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](bb054938c5))
2022-11-15 12:31:03 +00:00
Allan Bowe
a39f5dd9f1 Merge pull request #316 from sasjs/access-token-expiration
fix: change the expiration of access/refresh tokens from days to seconds
2022-11-15 12:25:41 +00:00
Allan Bowe
3ea444756c Update Client.ts 2022-11-15 11:00:42 +00:00
Allan Bowe
96399ecbbe Update swagger.yaml 2022-11-15 10:54:52 +00:00
bb054938c5 fix: change the expiration of access/refresh tokens from days to seconds 2022-11-15 15:48:03 +05:00
semantic-release-bot
fb6a556630 chore(release): 0.26.0 [skip ci]
# [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](acc25cbd68))

### Features

* make access token duration configurable when creating client/secret ([2413c05](2413c05fea))
* make refresh token duration configurable ([abd5c64](abd5c64b4a))
2022-11-13 14:04:03 +00:00
Allan Bowe
9dbd8e16bd Merge pull request #315 from sasjs/issue-307
feat: make access token duration configurable when creating client
2022-11-13 14:00:03 +00:00
fe07c41f5f chore: update header 2022-11-11 15:35:24 +05:00
acc25cbd68 fix(web): dispose monaco editor actions in return of useEffect 2022-11-11 15:27:12 +05:00
4ca61feda6 chore: npm audit fix 2022-11-10 21:05:41 +05:00
abd5c64b4a feat: make refresh token duration configurable 2022-11-10 21:02:20 +05:00
2413c05fea feat: make access token duration configurable when creating client/secret 2022-11-10 19:43:06 +05:00
semantic-release-bot
4c874c2c39 chore(release): 0.25.1 [skip ci]
## [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](c51b50428f))
2022-11-07 15:50:02 +00:00
Allan Bowe
d819d79bc9 Merge pull request #313 from sasjs/tree-view
fix(web): use mui treeView instead of custom implementation
2022-11-07 15:46:14 +00:00
c51b50428f fix(web): use mui treeView instead of custom implementation 2022-11-06 01:14:58 +05:00
semantic-release-bot
e10a0554f0 chore(release): 0.25.0 [skip ci]
# [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](1c9d167f86))
2022-11-02 15:24:25 +00:00
Allan Bowe
337e2eb2a0 Merge pull request #311 from sasjs/issue-310
feat: Enable DRIVE_LOCATION setting for deploying multiple instances
2022-11-02 15:19:54 +00:00
66f8e7840b chore: update readme 2022-11-02 20:18:28 +05:00
1c9d167f86 feat: Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server 2022-11-02 20:05:12 +05:00
semantic-release-bot
7e684b54a6 chore(release): 0.24.0 [skip ci]
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)

### Features

* cli mock testing ([6434123](6434123401))
* mocking sas9 responses with JS STP ([36be3a7](36be3a7d5e))
2022-10-28 10:05:48 +00:00
Sabir Hassan
aafda2922b Merge pull request #306 from sasjs/sas9-tests-mock-dynamic
feat: cli mock testing
2022-10-28 15:01:00 +05:00
418bf41e38 style: lint 2022-10-28 11:53:42 +02:00
81f0b03b09 chore: comments address 2022-10-28 11:53:25 +02:00
fe5ae44aab chore: typo 2022-10-17 18:32:58 +02:00
36be3a7d5e feat: mocking sas9 responses with JS STP 2022-10-17 18:31:08 +02:00
6434123401 feat: cli mock testing 2022-10-11 18:37:20 +02:00
semantic-release-bot
0a6b972c65 chore(release): 0.23.4 [skip ci]
## [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](2412622367))
2022-10-11 15:26:38 +00:00
Allan Bowe
be11707042 Merge pull request #303 from sasjs/issue-301
fix: add action to editor ref for running code
2022-10-11 16:08:57 +01:00
2412622367 fix: add action to editor ref for running code 2022-10-10 16:51:46 +05:00
semantic-release-bot
de3a190a8d chore(release): 0.23.3 [skip ci]
## [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](94072c3d24))
2022-10-09 20:32:07 +00:00
Allan Bowe
d5daafc6ed Merge pull request #302 from sasjs/cookies-with-domain
fix: added domain for session cookies
2022-10-09 21:26:40 +01:00
Saad Jutt
b1a2677b8c chore: specified domain for cookie for csrf as well 2022-10-10 00:48:13 +05:00
Saad Jutt
94072c3d24 fix: added domain for session cookies 2022-10-09 22:08:01 +05:00
semantic-release-bot
b64c0c12da chore(release): 0.23.2 [skip ci]
## [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](14731e8824))
* bumping sasjs/score ([258cc35](258cc35f14))
* reverting commit ([fda0e0b](fda0e0b57d))
2022-10-06 12:41:15 +00:00
Allan Bowe
79bc7b0e28 Merge pull request #300 from sasjs/corebump
fix: bumping sasjs/score
2022-10-06 13:36:20 +01:00
Allan Bowe
fda0e0b57d fix: reverting commit 2022-10-06 12:35:59 +00:00
Allan Bowe
14731e8824 fix: bump in correct place 2022-10-06 12:34:48 +00:00
Allan Bowe
258cc35f14 fix: bumping sasjs/score 2022-10-06 12:32:13 +00:00
semantic-release-bot
2295a518f0 chore(release): 0.23.1 [skip ci]
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)

### Bug Fixes

* ldap issues ([4d64420](4d64420c45))
2022-10-04 16:54:37 +00:00
Allan Bowe
1e5d621817 Merge pull request #298 from sasjs/fix-ldap
fix: ldap issues
2022-10-03 18:49:53 +01:00
4d64420c45 fix: ldap issues
logic fixed for updating user created by external auth provider
remove internal from AuthProviderType
replace AUTH_MECHANISM with AUTH_PROVIDERS
2022-10-03 21:24:10 +05:00
47 changed files with 615 additions and 306 deletions

2
.gitignore vendored
View File

@@ -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/

View File

@@ -1,3 +1,75 @@
## [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)
### 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) # [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)

View File

@@ -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
# #

View File

@@ -1,5 +1,6 @@
MODE=[desktop|server] default considered as desktop MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE 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`> WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http PROTOCOL=[http|https] default considered as http
@@ -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 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_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron> LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
@@ -29,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

View File

View File

@@ -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" />

26
api/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.2",
"dependencies": { "dependencies": {
"@sasjs/core": "^4.31.3", "@sasjs/core": "^4.40.1",
"@sasjs/utils": "2.48.1", "@sasjs/utils": "2.48.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
@@ -1394,9 +1394,9 @@
} }
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "4.31.3", "version": "4.40.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.31.3.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.40.1.tgz",
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA==" "integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.48.1", "version": "2.48.1",
@@ -7092,9 +7092,9 @@
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.0.4", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -11135,9 +11135,9 @@
} }
}, },
"@sasjs/core": { "@sasjs/core": {
"version": "4.31.3", "version": "4.40.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.31.3.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.40.1.tgz",
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA==" "integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.48.1", "version": "2.48.1",
@@ -15592,9 +15592,9 @@
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
}, },
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }

View File

@@ -48,7 +48,7 @@
}, },
"author": "4GL Ltd", "author": "4GL Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "^4.31.3", "@sasjs/core": "^4.40.1",
"@sasjs/utils": "2.48.1", "@sasjs/utils": "2.48.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",

View File

@@ -57,6 +57,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
@@ -680,7 +690,11 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString} value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.' 'Example 2':
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}
'Example 3':
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}
summary: "Admin only task. Create client with the following attributes:\nClientId,\nClientSecret,\naccessTokenExpiryDays (optional),\nrefreshTokenExpiryDays (optional)"
tags: tags:
- Client - Client
security: security:

View File

@@ -1,10 +1,9 @@
import { Express } from 'express' import { Express, CookieOptions } from 'express'
import mongoose from 'mongoose' import mongoose from 'mongoose'
import session from 'express-session' import session from 'express-session'
import MongoStore from 'connect-mongo' import MongoStore from 'connect-mongo'
import { ModeType } from '../utils' import { ModeType, ProtocolType } from '../utils'
import { cookieOptions } from '../app'
export const configureExpressSession = (app: Express) => { export const configureExpressSession = (app: Express) => {
const { MODE } = process.env 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( app.use(
session({ session({
secret: process.secrets.SESSION_SECRET, secret: process.secrets.SESSION_SECRET,

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import express, { ErrorRequestHandler, CookieOptions } from 'express' import express, { ErrorRequestHandler } from 'express'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import dotenv from 'dotenv' import dotenv from 'dotenv'
@@ -8,10 +8,10 @@ import {
getWebBuildFolder, getWebBuildFolder,
instantiateLogger, instantiateLogger,
loadAppStreamConfig, loadAppStreamConfig,
ProtocolType,
ReturnCode, ReturnCode,
setProcessVariables, setProcessVariables,
setupFolders, setupFolders,
setupUserAutoExec,
verifyEnvVariables verifyEnvVariables
} from './utils' } from './utils'
import { import {
@@ -29,15 +29,6 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express() 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) => { const onError: ErrorRequestHandler = (err, req, res, next) => {
console.error(err.stack) console.error(err.stack)
res.status(500).send('Something broke!') 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 // 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 setupUserAutoExec()
if (process.driveLoc === path.join(process.sasjsRoot, 'drive')) {
await setupFolders() await setupFolders()
await copySASjsCore() await copySASjsCore()
}
// 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

View File

@@ -8,6 +8,7 @@ import {
removeTokensInDB, removeTokensInDB,
saveTokensInDB saveTokensInDB
} from '../utils' } from '../utils'
import Client from '../model/Client'
@Route('SASjsApi/auth') @Route('SASjsApi/auth')
@Tags('Auth') @Tags('Auth')
@@ -83,8 +84,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 +102,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,

View File

@@ -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,
* accessTokenExpiryDays (optional),
* refreshTokenExpiryDays (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
} }
} }

View File

@@ -251,7 +251,7 @@ const updateUsersListInGroup = async (
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.` message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
} }
if (group.authProvider !== AuthProviderType.Internal) if (group.authProvider)
throw { throw {
code: 405, code: 405,
status: 'Method Not Allowed', status: 'Method Not Allowed',
@@ -266,7 +266,7 @@ const updateUsersListInGroup = async (
message: 'User not found.' message: 'User not found.'
} }
if (user.authProvider !== AuthProviderType.Internal) if (user.authProvider)
throw { throw {
code: 405, code: 405,
status: 'Method Not Allowed', status: 'Method Not Allowed',

View File

@@ -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)
@@ -104,7 +108,7 @@ export class ExecutionController {
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)
: '' : ''

View File

@@ -110,17 +110,13 @@ export const processProgram = async (
// 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 = fs.createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor // waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open') await once(writeStream, 'open')
execFileSync(executablePath, [codePath], { execFileSync(executablePath, [codePath], {
stdio: ['ignore', writeStream, writeStream] stdio: ['ignore', writeStream, writeStream]
}) })
// 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 session.completed = true
console.log('session completed', session) console.log('session completed', session)
} catch (err: any) { } catch (err: any) {

View File

@@ -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) {
console.log('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,
const content = await getMockResponseFromFile([ ...req.body,
process.cwd(), _requestMethod: req.method,
'mocks', _driveLoc: process.driveLoc
...program.split('/')
])
if (content.error) {
return content
} }
const filesNamesMap = req.files?.length
const parsedContent = parseJsonIfValid(content.content) ? 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
})
return { return {
content: parsedContent content: result.result as string
}
} catch (err) {
console.log('err', err)
}
return {
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> => {

View File

@@ -299,14 +299,19 @@ const updateUser = async (
const user = await User.findOne(findBy) const user = await User.findOne(findBy)
if ( if (username && username !== user?.username && user?.authProvider) {
user?.authProvider !== AuthProviderType.Internal &&
(username !== user?.username || displayName !== user?.displayName)
) {
throw { throw {
code: 405, code: 405,
message: 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.'
} }
} }

View File

@@ -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
} }
}) })

View File

@@ -50,8 +50,7 @@ const groupSchema = new Schema<IGroupDocument>({
}, },
authProvider: { authProvider: {
type: String, type: String,
enum: AuthProviderType, enum: AuthProviderType
default: 'internal'
}, },
isActive: { isActive: {
type: Boolean, type: Boolean,

View File

@@ -71,8 +71,7 @@ const userSchema = new Schema<IUserDocument>({
}, },
authProvider: { authProvider: {
type: String, type: String,
enum: AuthProviderType, enum: AuthProviderType
default: 'internal'
}, },
isAdmin: { isAdmin: {
type: Boolean, type: Boolean,

View File

@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
appStreamRouter(req, res, next) appStreamRouter(req, res, next)
}) })
app.use('/', csrfProtection, webRouter) app.use('/', webRouter)
} }

View File

@@ -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)
} }
} }

View File

@@ -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()

View File

@@ -14,7 +14,10 @@ webRouter.get('/', async (req, res) => {
} catch (_) { } catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>' response = '<html><head></head><body>Web Build is not present</body></html>'
} finally { } 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( const injectedContent = response?.replace(
'</head>', '</head>',
`${codeToInject}</head>` `${codeToInject}</head>`

View File

@@ -5,6 +5,7 @@ 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

View File

@@ -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).') console.log('Copying Macros from container to drive.')
const macrosDrivePath = getMacrosFolder() const macrosDrivePath = getMacrosFolder()

View File

@@ -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')

View File

@@ -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
}) })

View File

@@ -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
}) })

View File

@@ -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
} }

View File

@@ -26,6 +26,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'

View File

@@ -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)

View File

@@ -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 setupFolders = async () => {
const drivePath = getFilesFolder() await createFolder(getFilesFolder())
await createFolder(drivePath)
await createFolder(getPackagesFolder()) await createFolder(getPackagesFolder())
if (process.env.MODE === ModeType.Desktop) {
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
await createFile(getDesktopUserAutoExecPath(), '')
}
}
} }

View 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(), '')
}
}
}

View File

@@ -88,7 +88,9 @@ export const updateUserValidation = (
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 =>

View File

@@ -9,8 +9,7 @@ export enum ModeType {
} }
export enum AuthProviderType { export enum AuthProviderType {
LDAP = 'ldap', LDAP = 'ldap'
Internal = 'internal'
} }
export enum ProtocolType { export enum ProtocolType {
@@ -111,7 +110,7 @@ const verifyMODE = (): string[] => {
} }
if (process.env.MODE === ModeType.Server) { 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 (process.env.NODE_ENV !== 'test') {
if (!DB_CONNECT) if (!DB_CONNECT)
@@ -119,14 +118,12 @@ const verifyMODE = (): string[] => {
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'` `- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
) )
if (AUTH_MECHANISM) { if (AUTH_PROVIDERS) {
const authMechanismTypes = Object.values(AuthProviderType) const authProvidersType = Object.values(AuthProviderType)
if (!authMechanismTypes.includes(AUTH_MECHANISM as AuthProviderType)) if (!authProvidersType.includes(AUTH_PROVIDERS as AuthProviderType))
errors.push( 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 return errors
} }
const verifyExecutablePaths = () => { const verifyExecutablePaths = (): string[] => {
const errors: string[] = [] const errors: string[] = []
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } = const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
process.env process.env
@@ -307,37 +304,37 @@ const verifyLDAPVariables = () => {
LDAP_USERS_BASE_DN, LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN, LDAP_GROUPS_BASE_DN,
MODE, MODE,
AUTH_MECHANISM AUTH_PROVIDERS
} = process.env } = process.env
if (MODE === ModeType.Server && AUTH_MECHANISM === AuthProviderType.LDAP) { if (MODE === ModeType.Server && AUTH_PROVIDERS === AuthProviderType.LDAP) {
if (!LDAP_URL) { if (!LDAP_URL) {
errors.push( errors.push(
`- LDAP_URL is required for AUTH_MECHANISM '${AuthProviderType.LDAP}'` `- LDAP_URL is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
) )
} }
if (!LDAP_BIND_DN) { if (!LDAP_BIND_DN) {
errors.push( 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) { if (!LDAP_BIND_PASSWORD) {
errors.push( 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) { if (!LDAP_USERS_BASE_DN) {
errors.push( 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) { if (!LDAP_GROUPS_BASE_DN) {
errors.push( 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 = { const DEFAULTS = {
MODE: ModeType.Desktop, MODE: ModeType.Desktop,
AUTH_MECHANISM: AuthProviderType.Internal,
PROTOCOL: ProtocolType.HTTP, PROTOCOL: ProtocolType.HTTP,
PORT: '5000', PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE, HELMET_COEP: HelmetCoepType.TRUE,

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}
>
<TreeViewNode
node={node}
selectedFilePath={selectedFilePath}
handleSelect={handleSelect} handleSelect={handleSelect}
deleteNode={deleteNode} deleteNode={deleteNode}
addFile={addFile} addFile={addFile}
addFolder={addFolder} addFolder={addFolder}
rename={rename} 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 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

View File

@@ -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',

View File

@@ -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,7 +147,8 @@ 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(
(code: string) => {
setIsLoading(true) setIsLoading(true)
axios axios
.post(`/SASjsApi/code/execute`, { .post(`/SASjsApi/code/execute`, {
@@ -178,23 +178,16 @@ const useEditor = ({
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))
@@ -277,7 +296,6 @@ const useEditor = ({
}, [selectedFileExtension, runTimes]) }, [selectedFileExtension, runTimes])
return { return {
ctrlPressed,
fileContent, fileContent,
isLoading, isLoading,
log, log,
@@ -293,8 +311,6 @@ const useEditor = ({
handleDiffEditorDidMount, handleDiffEditorDidMount,
handleEditorDidMount, handleEditorDidMount,
handleFilePathInput, handleFilePathInput,
handleKeyDown,
handleKeyUp,
handleRunBtnClick, handleRunBtnClick,
handleTabChange, handleTabChange,
saveFile, saveFile,

View File

@@ -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}