1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-12 03:54:34 +00:00

Compare commits

...

30 Commits

Author SHA1 Message Date
semantic-release-bot
a131adbae7 chore(release): 0.9.0 [skip ci]
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)

### Features

* removed secrets from env variables ([9c3da56](9c3da56901))
2022-07-03 10:40:36 +00:00
Allan Bowe
a20c3b9719 Merge pull request #220 from sasjs/issue213
feat: removed secrets from env variables
2022-07-03 11:36:24 +01:00
Saad Jutt
eee3a7b084 chore: code refactor 2022-07-03 07:03:15 +05:00
Saad Jutt
9c3da56901 feat: removed secrets from env variables 2022-07-03 06:56:18 +05:00
Allan Bowe
7e6524d7e4 chore: removing badge 2022-07-02 15:12:27 +01:00
Allan Bowe
0ea2690616 adding matrix chat link 2022-07-02 13:10:20 +01:00
semantic-release-bot
b369759f0f chore(release): 0.8.3 [skip ci]
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)

### Bug Fixes

* **deploy:** extract first json from zip file ([e290751](e290751c87))
2022-07-02 10:01:26 +00:00
Allan Bowe
ac9a835c5a Merge pull request #219 from sasjs/issue211
fix(deploy): extract first json from zip file
2022-07-02 10:57:16 +01:00
Saad Jutt
e290751c87 fix(deploy): extract first json from zip file 2022-07-02 14:39:33 +05:00
semantic-release-bot
71bcbb9134 chore(release): 0.8.2 [skip ci]
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)

### Bug Fixes

* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](5cc85b57f8))
2022-06-22 10:18:59 +00:00
Allan Bowe
c86f0feff8 Merge pull request #214 from sasjs/fix-runtime-filePath
fix: getRuntimeAndFilePath function
2022-06-22 12:14:12 +02:00
Allan Bowe
d3d2ab9a36 Update getRunTimeAndFilePath.ts 2022-06-22 11:12:48 +01:00
5cc85b57f8 fix: getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes 2022-06-22 14:24:06 +05:00
semantic-release-bot
ae0fc0c48c chore(release): 0.8.1 [skip ci]
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)

### Bug Fixes

* make CA_ROOT optional in getCertificates method ([1b5859e](1b5859ee37))
* update /logout route to /SASLogon/logout ([65380be](65380be2f3))
2022-06-21 20:10:31 +00:00
Saad Jutt
555c5d54e2 Merge pull request #212 from sasjs/update-logout-route
Update logout route
2022-06-21 13:06:30 -07:00
1b5859ee37 fix: make CA_ROOT optional in getCertificates method 2022-06-22 00:25:41 +05:00
65380be2f3 fix: update /logout route to /SASLogon/logout 2022-06-22 00:24:41 +05:00
Yury Shkoda
1933be15c2 Merge pull request #210 from sasjs/pr-template
chore(template): added pull request template
2022-06-21 19:11:19 +03:00
Yury Shkoda
56b20beb8c chore(template): added pull request template 2022-06-21 19:07:14 +03:00
semantic-release-bot
bfc5ac6a4f chore(release): 0.8.0 [skip ci]
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)

### Features

* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](2119e9de9a))
2022-06-21 07:24:26 +00:00
Allan Bowe
6376173de0 Merge pull request #209 from sasjs/certificate-ca-root
feat(certs): ENV variables updated and set CA Root for HTTPS server
2022-06-21 09:18:15 +02:00
Saad Jutt
3130fbeff0 chore: code refactor for getting session controller 2022-06-21 03:41:44 +05:00
Saad Jutt
01e9a1d9e9 chore: README.md and .env.example updated 2022-06-21 03:26:12 +05:00
Saad Jutt
2119e9de9a feat(certs): ENV variables updated and set CA Root for HTTPS server 2022-06-21 03:17:14 +05:00
semantic-release-bot
87dbab98f6 chore(release): 0.7.3 [skip ci]
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)

### Bug Fixes

* path descriptions and defaults ([5d5d6ce](5d5d6ce326))
2022-06-20 15:31:20 +00:00
Allan Bowe
1bf122a0a2 Merge pull request #207 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: path descriptions and defaults
2022-06-20 17:26:56 +02:00
Allan Bowe
5d5d6ce326 fix: path descriptions and defaults 2022-06-20 15:07:17 +00:00
semantic-release-bot
620eddb713 chore(release): 0.7.2 [skip ci]
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)

### Bug Fixes

* removing UTF-8 options from commandline.  There appears to be no reliable way to enforce ([f6dc74f](f6dc74f16b))
2022-06-20 15:03:27 +00:00
Allan Bowe
3c92034da3 Merge pull request #206 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: removing UTF-8 options from commandline.
2022-06-20 16:57:58 +02:00
Allan Bowe
f6dc74f16b fix: removing UTF-8 options from commandline. There appears to be no reliable way to enforce
UTF-8 without additional modifications to the PATH variable to ensure a DBCS instance of SAS.
2022-06-20 14:38:19 +00:00
33 changed files with 491 additions and 324 deletions

View File

@@ -1,3 +1,53 @@
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)
### Features
* removed secrets from env variables ([9c3da56](https://github.com/sasjs/server/commit/9c3da56901672a818f54267f9defc9f4701ab7fb))
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
### Bug Fixes
* **deploy:** extract first json from zip file ([e290751](https://github.com/sasjs/server/commit/e290751c872d24009482871a8c398e834357dcde))
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)
### Bug Fixes
* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](https://github.com/sasjs/server/commit/5cc85b57f80b13296156811fe966d7b37d45f213))
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)
### Bug Fixes
* make CA_ROOT optional in getCertificates method ([1b5859e](https://github.com/sasjs/server/commit/1b5859ee37ae73c419115b9debfd5141a79733de))
* update /logout route to /SASLogon/logout ([65380be](https://github.com/sasjs/server/commit/65380be2f3945bae559f1749064845b514447a53))
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)
### Features
* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](https://github.com/sasjs/server/commit/2119e9de9ab1e5ce1222658f554ac74f4f35cf4d))
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)
### Bug Fixes
* path descriptions and defaults ([5d5d6ce](https://github.com/sasjs/server/commit/5d5d6ce3265a43af2e22bcd38cda54fafaf7b3ef))
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)
### Bug Fixes
* removing UTF-8 options from commandline. There appears to be no reliable way to enforce ([f6dc74f](https://github.com/sasjs/server/commit/f6dc74f16bddafa1de9c83c2f27671a241abdad4))
## [0.7.1](https://github.com/sasjs/server/compare/v0.7.0...v0.7.1) (2022-06-20) ## [0.7.1](https://github.com/sasjs/server/compare/v0.7.0...v0.7.1) (2022-06-20)

19
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,19 @@
## Issue
Link any related issue(s) in this section.
## Intent
What this PR intends to achieve.
## Implementation
What code changes have been made to achieve the intent.
## Checks
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] Any new functionality has been unit tested.
- [ ] All unit tests are passing (`npm test`).
- [ ] All CI checks are green.
- [ ] Reviewer is assigned.

View File

@@ -99,15 +99,12 @@ SASV9_OPTIONS= -NOXCMD
## Additional Web Server Options ## Additional Web Server Options
# #
# ENV variables required for PROTOCOL: `https` # ENV variables for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem (required)
FULL_CHAIN=fullchain.pem CERT_CHAIN=certificate.pem (required)
CA_ROOT=fullchain.pem (optional)
# ENV variables required for MODE: `server` # ENV variables required for MODE: `server`
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop` # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`

View File

@@ -4,17 +4,14 @@ WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem CERT_CHAIN=certificate.pem
CA_ROOT=fullchain.pem
PORT=[5000] default value is 5000 PORT=[5000] default value is 5000
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
HELMET_COEP=[true|false] if omitted HELMET default will be used HELMET_COEP=[true|false] if omitted HELMET default will be used
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas

View File

@@ -47,41 +47,6 @@ components:
- userId - userId
type: object type: object
additionalProperties: false additionalProperties: false
LoginPayload:
properties:
username:
type: string
description: 'Username for user'
example: secretuser
password:
type: string
description: 'Password for user'
example: secretpassword
required:
- username
- password
type: object
additionalProperties: false
AuthorizeResponse:
properties:
code:
type: string
description: 'Authorization code'
example: someRandomCryptoString
required:
- code
type: object
additionalProperties: false
AuthorizePayload:
properties:
clientId:
type: string
description: 'Client ID'
example: clientID1
required:
- clientId
type: object
additionalProperties: false
ClientPayload: ClientPayload:
properties: properties:
clientId: clientId:
@@ -440,13 +405,13 @@ components:
type: object type: object
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__: Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
properties: properties:
id:
description: 'The string version of this documents _id.'
_id: _id:
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__' $ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
description: 'This documents _id.' description: 'This documents _id.'
__v: __v:
description: 'This documents __v.' description: 'This documents __v.'
id:
description: 'The string version of this documents _id.'
type: object type: object
description: 'From T, pick a set of properties whose keys are in the union K' description: 'From T, pick a set of properties whose keys are in the union K'
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_: Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
@@ -488,6 +453,41 @@ components:
example: /Public/somefolder/some.file example: /Public/somefolder/some.file
type: object type: object
additionalProperties: false additionalProperties: false
LoginPayload:
properties:
username:
type: string
description: 'Username for user'
example: secretuser
password:
type: string
description: 'Password for user'
example: secretpassword
required:
- username
- password
type: object
additionalProperties: false
AuthorizeResponse:
properties:
code:
type: string
description: 'Authorization code'
example: someRandomCryptoString
required:
- code
type: object
additionalProperties: false
AuthorizePayload:
properties:
clientId:
type: string
description: 'Client ID'
example: clientID1
required:
- clientId
type: object
additionalProperties: false
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
type: http type: http
@@ -558,86 +558,6 @@ paths:
- -
bearerAuth: [] bearerAuth: []
parameters: [] parameters: []
/:
get:
operationId: Home
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
summary: 'Render index.html'
tags:
- Web
security: []
parameters: []
/SASLogon/login:
post:
operationId: Login
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
loggedIn: {type: boolean}
required:
- user
- loggedIn
type: object
summary: 'Accept a valid username/password'
tags:
- Web
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginPayload'
/SASLogon/authorize:
post:
operationId: Authorize
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizeResponse'
examples:
'Example 1':
value: {code: someRandomCryptoString}
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
tags:
- Web
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizePayload'
/logout:
get:
operationId: Logout
responses:
'200':
description: Ok
content:
application/json:
schema: {}
summary: 'Accept a valid username/password'
tags:
- Web
security: []
parameters: []
/SASjsApi/client: /SASjsApi/client:
post: post:
operationId: CreateClient operationId: CreateClient
@@ -763,7 +683,7 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'Deployment failed!'} value: {status: failure, message: 'Deployment failed!'}
description: "Accepts JSON file and zipped compressed JSON file as well.\r\nCompressed file should only contain one JSON file and should have same name\r\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\r\nAny other file or JSON file in zipped will be ignored!" description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!"
summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.' summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
tags: tags:
- Drive - Drive
@@ -851,7 +771,7 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'File request failed.'} value: {status: failure, message: 'File request failed.'}
description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request." description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Create a file in SASjs Drive' summary: 'Create a file in SASjs Drive'
tags: tags:
- Drive - Drive
@@ -902,7 +822,7 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'File request failed.'} value: {status: failure, message: 'File request failed.'}
description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request." description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Modify a file in SASjs Drive' summary: 'Modify a file in SASjs Drive'
tags: tags:
- Drive - Drive
@@ -1454,7 +1374,7 @@ paths:
anyOf: anyOf:
- {type: string} - {type: string}
- {type: string, format: byte} - {type: string, format: byte}
description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms" description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
summary: 'Execute a Stored Program, returns raw _webout content.' summary: 'Execute a Stored Program, returns raw _webout content.'
tags: tags:
- STP - STP
@@ -1482,7 +1402,7 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}} value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms\r\n\r\nThe response will be a JSON object with the following root attributes:\r\nlog, webout, headers.\r\n\r\nThe webout attribute will be nested JSON ONLY if the response-header\r\ncontains a content-type of application/json AND it is valid JSON.\r\nOtherwise it will be a stringified version of the webout content." description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content."
summary: 'Execute a Stored Program, return a JSON object' summary: 'Execute a Stored Program, return a JSON object'
tags: tags:
- STP - STP
@@ -1504,6 +1424,86 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteReturnJsonPayload' $ref: '#/components/schemas/ExecuteReturnJsonPayload'
/:
get:
operationId: Home
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
summary: 'Render index.html'
tags:
- Web
security: []
parameters: []
/SASLogon/login:
post:
operationId: Login
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
loggedIn: {type: boolean}
required:
- user
- loggedIn
type: object
summary: 'Accept a valid username/password'
tags:
- Web
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginPayload'
/SASLogon/authorize:
post:
operationId: Authorize
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizeResponse'
examples:
'Example 1':
value: {code: someRandomCryptoString}
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
tags:
- Web
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizePayload'
/SASLogon/logout:
get:
operationId: Logout
responses:
'200':
description: Ok
content:
application/json:
schema: {}
summary: 'Destroy the session stored in cookies'
tags:
- Web
security: []
parameters: []
servers: servers:
- -
url: / url: /

View File

@@ -1,5 +1,6 @@
import path from 'path' import path from 'path'
import express, { ErrorRequestHandler } from 'express' import express, { ErrorRequestHandler } from 'express'
import mongoose from 'mongoose'
import csrf from 'csurf' import csrf from 'csurf'
import session from 'express-session' import session from 'express-session'
import MongoStore from 'connect-mongo' import MongoStore from 'connect-mongo'
@@ -97,45 +98,44 @@ if (CORS === CorsType.ENABLED) {
app.use(cors({ credentials: true, origin: whiteList })) app.use(cors({ credentials: true, origin: whiteList }))
} }
/*********************************** export default setProcessVariables().then(async () => {
* DB Connection & * /***********************************
* Express Sessions * * DB Connection & *
* With Mongo Store * * Express Sessions *
***********************************/ * With Mongo Store *
if (MODE === ModeType.Server) { ***********************************/
let store: MongoStore | undefined if (MODE === ModeType.Server) {
let store: MongoStore | undefined
// NOTE: when exporting app.js as agent for supertest if (process.env.NODE_ENV !== 'test') {
// we should exclude connecting to the real database store = MongoStore.create({
if (process.env.NODE_ENV !== 'test') { client: mongoose.connection!.getClient() as any,
const clientPromise = connectDB().then((conn) => conn!.getClient() as any) collectionName: 'sessions'
})
}
store = MongoStore.create({ clientPromise, collectionName: 'sessions' }) app.use(
session({
secret: process.secrets.SESSION_SECRET,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store,
cookie: cookieOptions
})
)
} }
app.use( app.use(express.json({ limit: '100mb' }))
session({ app.use(express.static(path.join(__dirname, '../public')))
secret: process.env.SESSION_SECRET as string,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store,
cookie: cookieOptions
})
)
}
app.use(express.json({ limit: '100mb' })) const onError: ErrorRequestHandler = (err, req, res, next) => {
app.use(express.static(path.join(__dirname, '../public'))) if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
const onError: ErrorRequestHandler = (err, req, res, next) => { console.error(err.stack)
if (err.code === 'EBADCSRFTOKEN') res.status(500).send('Something broke!')
return res.status(400).send('Invalid CSRF token!') }
console.error(err.stack)
res.status(500).send('Something broke!')
}
export default setProcessVariables().then(async () => {
await setupFolders() await setupFolders()
await copySASjsCore() await copySASjsCore()

View File

@@ -129,8 +129,8 @@ const verifyAuthCode = async (
clientId: string, clientId: string,
code: string code: string
): Promise<InfoJWT | undefined> => { ): Promise<InfoJWT | undefined> => {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => { jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
if (err) return resolve(undefined) if (err) return resolve(undefined)
const clientInfo: InfoJWT = { const clientInfo: InfoJWT = {

View File

@@ -20,7 +20,7 @@ export interface GroupResponse {
description: string description: string
} }
interface GroupDetailsResponse { export interface GroupDetailsResponse {
groupId: number groupId: number
name: string name: string
description: string description: string
@@ -249,9 +249,10 @@ const updateUsersListInGroup = async (
message: 'User not found.' message: 'User not found.'
} }
const updatedGroup = (action === 'addUser' const updatedGroup =
? await group.addUser(user._id) action === 'addUser'
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse ? await group.addUser(user)
: await group.removeUser(user)
if (!updatedGroup) if (!updatedGroup)
throw { throw {
@@ -260,9 +261,6 @@ const updateUsersListInGroup = async (
message: 'Unable to update group.' message: 'Unable to update group.'
} }
if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id)
return { return {
groupId: updatedGroup.groupId, groupId: updatedGroup.groupId,
name: updatedGroup.name, name: updatedGroup.name,

View File

@@ -1,10 +1,6 @@
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { import { getSessionController, processProgram } from './'
getSASSessionController,
getJSSessionController,
processProgram
} from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils' import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types' import { PreProgramVars, Session, TreeNode } from '../../types'
import { import {
@@ -76,10 +72,7 @@ export class ExecutionController {
session: sessionByFileUpload, session: sessionByFileUpload,
runTime runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> { }: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController = const sessionController = getSessionController(runTime)
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
const session = const session =
sessionByFileUpload ?? (await sessionController.getSession()) sessionByFileUpload ?? (await sessionController.getSession())

View File

@@ -1,7 +1,7 @@
import { Request, RequestHandler } from 'express' import { Request, RequestHandler } from 'express'
import multer from 'multer' import multer from 'multer'
import { uuidv4 } from '@sasjs/utils' import { uuidv4 } from '@sasjs/utils'
import { getSASSessionController, getJSSessionController } from '.' import { getSessionController } from '.'
import { import {
executeProgramRawValidation, executeProgramRawValidation,
getRunTimeAndFilePath, getRunTimeAndFilePath,
@@ -37,17 +37,23 @@ export class FileUploadController {
try { try {
;({ runTime } = await getRunTimeAndFilePath(programPath)) ;({ runTime } = await getRunTimeAndFilePath(programPath))
} catch (err: any) { } catch (err: any) {
res.status(400).send({ return res.status(400).send({
status: 'failure', status: 'failure',
message: 'Job execution failed', message: 'Job execution failed',
error: typeof err === 'object' ? err.toString() : err error: typeof err === 'object' ? err.toString() : err
}) })
} }
const sessionController = let sessionController
runTime === RunTimeType.SAS try {
? getSASSessionController() sessionController = getSessionController(runTime)
: getJSSessionController() } catch (err: any) {
return res.status(400).send({
status: 'failure',
message: err.message,
error: typeof err === 'object' ? err.toString() : err
})
}
const session = await sessionController.getSession() const session = await sessionController.getSession()
// marking consumed true, so that it's not available // marking consumed true, so that it's not available

View File

@@ -5,14 +5,16 @@ import { execFile } from 'child_process'
import { import {
getSessionsFolder, getSessionsFolder,
generateUniqueFileName, generateUniqueFileName,
sysInitCompiledPath sysInitCompiledPath,
RunTimeType
} from '../../utils' } from '../../utils'
import { import {
deleteFolder, deleteFolder,
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile readFile,
isWindows
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -88,7 +90,7 @@ ${autoExecContent}`
// Additional windows specific options to avoid the desktop popups. // Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc, [ execFilePromise(process.sasLoc!, [
'-SYSIN', '-SYSIN',
codePath, codePath,
'-LOG', '-LOG',
@@ -99,11 +101,9 @@ ${autoExecContent}`
session.path, session.path,
'-AUTOEXEC', '-AUTOEXEC',
autoExecPath, autoExecPath,
'-ENCODING', isWindows() ? '-nosplash' : '',
'UTF-8', isWindows() ? '-icon' : '',
process.platform === 'win32' ? '-nosplash' : '', isWindows() ? '-nologo' : ''
process.platform === 'win32' ? '-icon' : '',
process.platform === 'win32' ? '-nologo' : ''
]) ])
.then(() => { .then(() => {
session.completed = true session.completed = true
@@ -194,7 +194,21 @@ export class JSSessionController extends SessionController {
} }
} }
export const getSASSessionController = (): SASSessionController => { export const getSessionController = (
runTime: RunTimeType
): SASSessionController | JSSessionController => {
if (runTime === RunTimeType.SAS) {
return getSASSessionController()
}
if (runTime === RunTimeType.JS) {
return getJSSessionController()
}
throw new Error('No Runtime is configured')
}
const getSASSessionController = (): SASSessionController => {
if (process.sasSessionController) return process.sasSessionController if (process.sasSessionController) return process.sasSessionController
process.sasSessionController = new SASSessionController() process.sasSessionController = new SASSessionController()
@@ -202,7 +216,7 @@ export const getSASSessionController = (): SASSessionController => {
return process.sasSessionController return process.sasSessionController
} }
export const getJSSessionController = (): JSSessionController => { const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController() process.jsSessionController = new JSSessionController()

View File

@@ -1,3 +1,4 @@
import { isWindows } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types' import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils' import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './' import { ExecutionVars } from './'
@@ -20,9 +21,7 @@ export const createJSProgram = async (
const preProgramVarStatments = ` const preProgramVarStatments = `
let _webout = ''; let _webout = '';
const weboutPath = '${ const weboutPath = '${
process.platform === 'win32' isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
? weboutPath.replace(/\\/g, '\\\\')
: weboutPath
}'; }';
const _sasjs_tokenfile = '${tokenFile}'; const _sasjs_tokenfile = '${tokenFile}';
const _sasjs_username = '${preProgramVariables?.username}'; const _sasjs_username = '${preProgramVariables?.username}';

View File

@@ -40,7 +40,7 @@ export const processProgram = async (
// waiting for the open event so that we can have underlying file descriptor // waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open') await once(writeStream, 'open')
execFileSync(process.nodeLoc, [codePath], { execFileSync(process.nodeLoc!, [codePath], {
stdio: ['ignore', writeStream, writeStream] stdio: ['ignore', writeStream, writeStream]
}) })

View File

@@ -49,10 +49,10 @@ export class WebController {
} }
/** /**
* @summary Accept a valid username/password * @summary Destroy the session stored in cookies
* *
*/ */
@Get('/logout') @Get('/SASLogon/logout')
public async logout(@Request() req: express.Request) { public async logout(@Request() req: express.Request) {
return new Promise((resolve) => { return new Promise((resolve) => {
req.session.destroy(() => { req.session.destroy(() => {

View File

@@ -35,7 +35,7 @@ export const authenticateAccessToken: RequestHandler = async (
req, req,
res, res,
next, next,
process.env.ACCESS_TOKEN_SECRET as string, process.secrets.ACCESS_TOKEN_SECRET,
'accessToken' 'accessToken'
) )
} }
@@ -45,7 +45,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
req, req,
res, res,
next, next,
process.env.REFRESH_TOKEN_SECRET as string, process.secrets.REFRESH_TOKEN_SECRET,
'refreshToken' 'refreshToken'
) )
} }

View File

@@ -0,0 +1,45 @@
import mongoose, { Schema } from 'mongoose'
export interface ConfigurationType {
/**
* SecretOrPrivateKey to sign Access Token
* @example "someRandomCryptoString"
*/
ACCESS_TOKEN_SECRET: string
/**
* SecretOrPrivateKey to sign Refresh Token
* @example "someRandomCryptoString"
*/
REFRESH_TOKEN_SECRET: string
/**
* SecretOrPrivateKey to sign Auth Code
* @example "someRandomCryptoString"
*/
AUTH_CODE_SECRET: string
/**
* Secret used to sign the session cookie
* @example "someRandomCryptoString"
*/
SESSION_SECRET: string
}
const ConfigurationSchema = new Schema<ConfigurationType>({
ACCESS_TOKEN_SECRET: {
type: String,
required: true
},
REFRESH_TOKEN_SECRET: {
type: String,
required: true
},
AUTH_CODE_SECRET: {
type: String,
required: true
},
SESSION_SECRET: {
type: String,
required: true
}
})
export default mongoose.model('Configuration', ConfigurationSchema)

View File

@@ -1,5 +1,6 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose' import mongoose, { Schema, model, Document, Model } from 'mongoose'
import User from './User' import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose) const AutoIncrement = require('mongoose-sequence')(mongoose)
export interface GroupPayload { export interface GroupPayload {
@@ -27,8 +28,9 @@ interface IGroupDocument extends GroupPayload, Document {
} }
interface IGroup extends IGroupDocument { interface IGroup extends IGroupDocument {
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup> addUser(user: IUser): Promise<GroupDetailsResponse>
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup> removeUser(user: IUser): Promise<GroupDetailsResponse>
hasUser(user: IUser): boolean
} }
interface IGroupModel extends Model<IGroup> {} interface IGroupModel extends Model<IGroup> {}
@@ -70,28 +72,31 @@ groupSchema.pre('remove', async function () {
}) })
// Instance Methods // Instance Methods
groupSchema.method( groupSchema.method('addUser', async function (user: IUser) {
'addUser', const userObjectId = user._id
async function (userObjectId: Schema.Types.ObjectId) { const userIdIndex = this.users.indexOf(userObjectId)
const userIdIndex = this.users.indexOf(userObjectId) if (userIdIndex === -1) {
if (userIdIndex === -1) { this.users.push(userObjectId)
this.users.push(userObjectId) user.addGroup(this._id)
}
this.markModified('users')
return this.save()
} }
) this.markModified('users')
groupSchema.method( return this.save()
'removeUser', })
async function (userObjectId: Schema.Types.ObjectId) { groupSchema.method('removeUser', async function (user: IUser) {
const userIdIndex = this.users.indexOf(userObjectId) const userObjectId = user._id
if (userIdIndex > -1) { const userIdIndex = this.users.indexOf(userObjectId)
this.users.splice(userIdIndex, 1) if (userIdIndex > -1) {
} this.users.splice(userIdIndex, 1)
this.markModified('users') user.removeGroup(this._id)
return this.save()
} }
) this.markModified('users')
return this.save()
})
groupSchema.method('hasUser', function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
return userIdIndex > -1
})
export const Group: IGroupModel = model<IGroup, IGroupModel>( export const Group: IGroupModel = model<IGroup, IGroupModel>(
'Group', 'Group',

View File

@@ -35,6 +35,7 @@ export interface UserPayload {
} }
interface IUserDocument extends UserPayload, Document { interface IUserDocument extends UserPayload, Document {
_id: Schema.Types.ObjectId
id: number id: number
isAdmin: boolean isAdmin: boolean
isActive: boolean isActive: boolean
@@ -43,7 +44,7 @@ interface IUserDocument extends UserPayload, Document {
tokens: [{ [key: string]: string }] tokens: [{ [key: string]: string }]
} }
interface IUser extends IUserDocument { export interface IUser extends IUserDocument {
comparePassword(password: string): boolean comparePassword(password: string): boolean
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser> addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser> removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>

View File

@@ -48,7 +48,7 @@ webRouter.post(
} }
) )
webRouter.get('/logout', desktopRestrict, async (req, res) => { webRouter.get('/SASLogon/logout', desktopRestrict, async (req, res) => {
try { try {
await controller.logout(req) await controller.logout(req)
res.status(200).send('OK!') res.status(200).send('OK!')

View File

@@ -16,9 +16,9 @@ appPromise.then(async (app) => {
) )
}) })
} else { } else {
const { key, cert } = await getCertificates() const { key, cert, ca } = await getCertificates()
const httpsServer = createServer({ key, cert }, app) const httpsServer = createServer({ key, cert, ca }, app)
httpsServer.listen(sasJsPort, () => { httpsServer.listen(sasJsPort, () => {
console.log( console.log(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}` `⚡️[server]: Server is running at https://localhost:${sasJsPort}`

View File

@@ -1,12 +1,13 @@
declare namespace NodeJS { declare namespace NodeJS {
export interface Process { export interface Process {
sasLoc: string sasLoc?: string
nodeLoc: string nodeLoc?: string
driveLoc: string driveLoc: string
sasSessionController?: import('../../controllers/internal').SASSessionController sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController jsSessionController?: import('../../controllers/internal').JSSessionController
appStreamConfig: import('../').AppStreamConfig appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[] runTimes: import('../../utils').RunTimeType[]
secrets: import('../../model/Configuration').ConfigurationType
} }
} }

View File

@@ -9,7 +9,5 @@ export const connectDB = async () => {
} }
console.log('Connected to DB!') console.log('Connected to DB!')
await seedDB() return seedDB()
return mongoose.connection
} }

View File

@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types' import { InfoJWT } from '../types'
export const generateAccessToken = (data: InfoJWT) => export const generateAccessToken = (data: InfoJWT) =>
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, { jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
expiresIn: '1day' expiresIn: '1day'
}) })

View File

@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types' import { InfoJWT } from '../types'
export const generateAuthCode = (data: InfoJWT) => export const generateAuthCode = (data: InfoJWT) =>
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, { jwt.sign(data, process.secrets.AUTH_CODE_SECRET, {
expiresIn: '30s' expiresIn: '30s'
}) })

View File

@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types' import { InfoJWT } from '../types'
export const generateRefreshToken = (data: InfoJWT) => export const generateRefreshToken = (data: InfoJWT) =>
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, { jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
expiresIn: '30 days' expiresIn: '30 days'
}) })

View File

@@ -2,22 +2,32 @@ import path from 'path'
import { fileExists, getString, readFile } from '@sasjs/utils' import { fileExists, getString, readFile } from '@sasjs/utils'
export const getCertificates = async () => { export const getCertificates = async () => {
const { PRIVATE_KEY, FULL_CHAIN } = process.env const { PRIVATE_KEY, CERT_CHAIN, CA_ROOT } = process.env
let ca
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)')) const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)')) const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
const caPath = CA_ROOT
console.log('keyPath: ', keyPath) console.log('keyPath: ', keyPath)
console.log('certPath: ', certPath) console.log('certPath: ', certPath)
if (caPath) console.log('caPath: ', caPath)
const key = await readFile(keyPath) const key = await readFile(keyPath)
const cert = await readFile(certPath) const cert = await readFile(certPath)
if (caPath) ca = await readFile(caPath)
return { key, cert } return { key, cert, ca }
} }
const getFileInput = async (filename: string): Promise<string> => { const getFileInput = async (
filename: string,
required: boolean = true
): Promise<string> => {
const validator = async (filePath: string) => { const validator = async (filePath: string) => {
if (!required) return true
if (!filePath) return `Path to ${filename} is required.` if (!filePath) return `Path to ${filename} is required.`
if (!(await fileExists(path.join(process.cwd(), filePath)))) { if (!(await fileExists(path.join(process.cwd(), filePath)))) {

View File

@@ -1,15 +1,20 @@
import path from 'path' import path from 'path'
import { getString } from '@sasjs/utils/input' import { getString } from '@sasjs/utils/input'
import { createFolder, fileExists, folderExists } from '@sasjs/utils' import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
import { RunTimeType } from './verifyEnvVariables'
const isWindows = () => process.platform === 'win32'
export const getDesktopFields = async () => { export const getDesktopFields = async () => {
const { SAS_PATH, NODE_PATH } = process.env const { SAS_PATH, NODE_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation()) let sasLoc, nodeLoc
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation()) if (process.runTimes.includes(RunTimeType.SAS)) {
sasLoc = SAS_PATH ?? (await getSASLocation())
}
if (process.runTimes.includes(RunTimeType.JS)) {
nodeLoc = NODE_PATH ?? (await getNodeLocation())
}
return { sasLoc, nodeLoc } return { sasLoc, nodeLoc }
} }
@@ -55,7 +60,7 @@ const getSASLocation = async (): Promise<string> => {
: '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas' : '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas'
const targetName = await getString( const targetName = await getString(
'Please enter path to SAS executable (absolute path): ', 'Please enter full path to a SAS executable with UTF-8 encoding: ',
validator, validator,
defaultLocation defaultLocation
) )
@@ -75,11 +80,11 @@ const getNodeLocation = async (): Promise<string> => {
} }
const defaultLocation = isWindows() const defaultLocation = isWindows()
? 'C:\\Program Files\\nodejs\\' ? 'C:\\Program Files\\nodejs\\node.exe'
: '/usr/local/nodejs/bin' : '/usr/local/nodejs/bin/node.sh'
const targetName = await getString( const targetName = await getString(
'Please enter path to nodejs executable (absolute path): ', 'Please enter full path to a NodeJS executable: ',
validator, validator,
defaultLocation defaultLocation
) )

View File

@@ -5,14 +5,10 @@ import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => { export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath) const ext = path.extname(programPath)
// if program path is provided with extension we should split that into code path and ext as run time // If programPath (_program) is provided with a ".sas" or ".js" extension
if (ext) { // we should use that extension to determine the appropriate runTime
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
const runTime = ext.slice(1) const runTime = ext.slice(1)
const runTimeTypes = Object.values(RunTimeType)
if (!runTimeTypes.includes(runTime as RunTimeType)) {
throw `The '${runTime}' runtime is not supported.`
}
const codePath = path const codePath = path
.join(getFilesFolder(), programPath) .join(getFilesFolder(), programPath)

View File

@@ -1,6 +1,73 @@
import Client from '../model/Client' import Client from '../model/Client'
import Group from '../model/Group'
import User from '../model/User' import User from '../model/User'
import Configuration, { ConfigurationType } from '../model/Configuration'
import { randomBytes } from 'crypto'
export const SECRETS: ConfigurationType = {
ACCESS_TOKEN_SECRET: randomBytes(64).toString('hex'),
REFRESH_TOKEN_SECRET: randomBytes(64).toString('hex'),
AUTH_CODE_SECRET: randomBytes(64).toString('hex'),
SESSION_SECRET: randomBytes(64).toString('hex')
}
export const seedDB = async (): Promise<ConfigurationType> => {
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
if (!clientExist) {
const client = new Client(CLIENT)
await client.save()
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
}
// Checking if 'AllUsers' Group is already in the database
let groupExist = await Group.findOne({ name: GROUP.name })
if (!groupExist) {
const group = new Group(GROUP)
groupExist = await group.save()
console.log(`DB Seed - Group created: ${GROUP.name}`)
}
// Checking if user is already in the database
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
if (!usernameExist) {
const user = new User(ADMIN_USER)
usernameExist = await user.save()
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
}
if (!groupExist.hasUser(usernameExist)) {
groupExist.addUser(usernameExist)
console.log(
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
)
}
// checking if configuration is present in the database
let configExist = await Configuration.findOne()
if (!configExist) {
const configuration = new Configuration(SECRETS)
configExist = await configuration.save()
console.log('DB Seed - configuration added')
}
return {
ACCESS_TOKEN_SECRET: configExist.ACCESS_TOKEN_SECRET,
REFRESH_TOKEN_SECRET: configExist.REFRESH_TOKEN_SECRET,
AUTH_CODE_SECRET: configExist.AUTH_CODE_SECRET,
SESSION_SECRET: configExist.SESSION_SECRET
}
}
const GROUP = {
name: 'AllUsers',
description: 'Group contains all users'
}
const CLIENT = { const CLIENT = {
clientId: 'clientID1', clientId: 'clientID1',
clientSecret: 'clientSecret' clientSecret: 'clientSecret'
@@ -13,23 +80,3 @@ const ADMIN_USER = {
isAdmin: true, isAdmin: true,
isActive: true isActive: true
} }
export const seedDB = async () => {
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
if (!clientExist) {
const client = new Client(CLIENT)
await client.save()
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
}
// Checking if user is already in the database
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
if (!usernameExist) {
const user = new User(ADMIN_USER)
await user.save()
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
}
}

View File

@@ -1,19 +1,33 @@
import path from 'path' import path from 'path'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils' import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
import { getDesktopFields, ModeType, RunTimeType } from '.' import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
export const setProcessVariables = async () => { export const setProcessVariables = async () => {
const { MODE, RUN_TIMES } = process.env
if (MODE === ModeType.Server) {
// NOTE: when exporting app.js as agent for supertest
// it should prevent connecting to the real database
if (process.env.NODE_ENV !== 'test') {
const secrets = await connectDB()
process.secrets = secrets
} else {
process.secrets = SECRETS
}
}
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
process.driveLoc = path.join(process.cwd(), 'sasjs_root') process.driveLoc = path.join(process.cwd(), 'sasjs_root')
return return
} }
const { MODE } = process.env process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
if (MODE === ModeType.Server) { if (MODE === ModeType.Server) {
process.sasLoc = process.env.SAS_PATH as string process.sasLoc = process.env.SAS_PATH
process.nodeLoc = process.env.NODE_PATH as string process.nodeLoc = process.env.NODE_PATH
} else { } else {
const { sasLoc, nodeLoc } = await getDesktopFields() const { sasLoc, nodeLoc } = await getDesktopFields()
@@ -26,9 +40,6 @@ export const setProcessVariables = async () => {
await createFolder(absPath) await createFolder(absPath)
process.driveLoc = getRealPath(absPath) process.driveLoc = getRealPath(absPath)
const { RUN_TIMES } = process.env
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
console.log('sasLoc: ', process.sasLoc) console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc) console.log('sasDrive: ', process.driveLoc)
console.log('runTimes: ', process.runTimes) console.log('runTimes: ', process.runTimes)

View File

@@ -78,33 +78,7 @@ const verifyMODE = (): string[] => {
} }
if (process.env.MODE === ModeType.Server) { if (process.env.MODE === ModeType.Server) {
const { const { DB_CONNECT } = process.env
ACCESS_TOKEN_SECRET,
REFRESH_TOKEN_SECRET,
AUTH_CODE_SECRET,
SESSION_SECRET,
DB_CONNECT
} = process.env
if (!ACCESS_TOKEN_SECRET)
errors.push(
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (!REFRESH_TOKEN_SECRET)
errors.push(
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (!AUTH_CODE_SECRET)
errors.push(
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (!SESSION_SECRET)
errors.push(
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (process.env.NODE_ENV !== 'test') if (process.env.NODE_ENV !== 'test')
if (!DB_CONNECT) if (!DB_CONNECT)
@@ -129,16 +103,16 @@ const verifyPROTOCOL = (): string[] => {
} }
if (process.env.PROTOCOL === ProtocolType.HTTPS) { if (process.env.PROTOCOL === ProtocolType.HTTPS) {
const { PRIVATE_KEY, FULL_CHAIN } = process.env const { PRIVATE_KEY, CERT_CHAIN } = process.env
if (!PRIVATE_KEY) if (!PRIVATE_KEY)
errors.push( errors.push(
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'` `- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
) )
if (!FULL_CHAIN) if (!CERT_CHAIN)
errors.push( errors.push(
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'` `- CERT_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
) )
} }
@@ -258,5 +232,5 @@ const DEFAULTS = {
PORT: '5000', PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE, HELMET_COEP: HelmetCoepType.TRUE,
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common, LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
RUN_TIMES: `${RunTimeType.SAS}` RUN_TIMES: RunTimeType.SAS
} }

View File

@@ -28,7 +28,8 @@ export const extractJSONFromZip = async (zipFile: Express.Multer.File) => {
for await (const entry of zip) { for await (const entry of zip) {
const fileName = entry.path as string const fileName = entry.path as string
if (fileName.toUpperCase().endsWith('.JSON') && fileName === fileInZip) { // grab the first json found in .zip
if (fileName.toUpperCase().endsWith('.JSON')) {
fileContent = await entry.buffer() fileContent = await entry.buffer()
break break
} else { } else {

View File

@@ -88,7 +88,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
}, []) }, [])
const logout = useCallback(() => { const logout = useCallback(() => {
axios.get('/logout').then(() => { axios.get('/SASLogon/logout').then(() => {
setLoggedIn(false) setLoggedIn(false)
setUsername('') setUsername('')
setDisplayName('') setDisplayName('')