mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ae8f35e9a | ||
|
|
667e26b080 | ||
|
|
d09876c05f | ||
|
|
fb8e18be75 | ||
|
|
7ac7a4e083 | ||
|
|
8e23786dd4 | ||
|
|
4bd01bcf29 | ||
|
|
51f6aa34a1 | ||
|
|
486207128d | ||
|
|
1e4b0b9171 | ||
|
|
1ff820605a | ||
|
|
9c1a781b3a | ||
| 36628551ae | |||
| 23cf8fa06f | |||
| 84ee743eae | |||
|
|
19e5bd7d2d | ||
|
|
e251747302 | ||
|
|
7e7558d4cf | ||
|
|
f02996facf | ||
|
|
803c51f400 | ||
|
|
c35b2b3f59 | ||
|
|
fe0866ace7 | ||
|
|
1513c3623d | ||
|
|
7fe43ae0b7 | ||
|
|
c4cea4a12b | ||
|
|
9fc7a132ba | ||
|
|
d55a619d64 | ||
|
|
737d2a24c2 | ||
|
|
2e63831b90 | ||
|
|
c7ffde1a3b | ||
|
|
db70b1ce55 | ||
|
|
8a3fe8b217 | ||
| 9dca552e82 | |||
|
|
505f2089c7 | ||
|
|
3344c400a8 | ||
| fa6248e3ef | |||
| 9fb5f1f8e7 |
73
.github/CONTRIBUTING.md
vendored
73
.github/CONTRIBUTING.md
vendored
@@ -2,25 +2,22 @@
|
||||
|
||||
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
|
||||
|
||||
The app can be deployed using Docker or NodeJS.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is made in the `configuration` section of `package.json`:
|
||||
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
|
||||
|
||||
- Provide path to SAS9 executable.
|
||||
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
|
||||
|
||||
* `.env` - the root .env file is used only for Docker deploys.
|
||||
* `api/.env` - this is the primary file used in NodeJS deploys
|
||||
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
|
||||
|
||||
|
||||
### Using dockers:
|
||||
## Using Docker
|
||||
|
||||
There is `.env.example` file present at root of the project. [for Production]
|
||||
|
||||
There is `.env.example` file present at `./api` of the project. [for Development]
|
||||
|
||||
There is `.env.example` file present at `./web` of the project. [for Development]
|
||||
|
||||
Remember to provide enviornment variables.
|
||||
|
||||
#### Development
|
||||
### Docker Development Mode
|
||||
|
||||
Command to run docker for development:
|
||||
|
||||
@@ -38,7 +35,7 @@ It will build following images if running first time:
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
|
||||
#### Production
|
||||
### Docker Production Mode
|
||||
|
||||
Command to run docker for production:
|
||||
|
||||
@@ -54,47 +51,45 @@ It will build following images if running first time:
|
||||
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
### Using node:
|
||||
## Using NodeJS:
|
||||
|
||||
#### Development (running api and web seperately):
|
||||
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
|
||||
|
||||
##### API
|
||||
### NodeJS Development Mode
|
||||
|
||||
Navigate to `./api`
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
|
||||
|
||||
### NodeJS Dev - Single Port
|
||||
|
||||
Here the environment variables should be configured under `api.env`. Then:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build
|
||||
cd ../api && npm i && npm start
|
||||
```
|
||||
|
||||
### NodeJS Dev - Seperate Ports
|
||||
|
||||
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
|
||||
|
||||
#### API server
|
||||
```
|
||||
cd api
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
##### Web
|
||||
|
||||
Navigate to `./web`
|
||||
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
#### Web Server
|
||||
|
||||
```
|
||||
cd web
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Development (running only api server and have web build served):
|
||||
#### NodeJS Production Mode
|
||||
|
||||
##### API server also serving Web build files
|
||||
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
cd ./api && npm i && npm start
|
||||
```
|
||||
|
||||
#### Production
|
||||
|
||||
##### API & WEB
|
||||
Update the `.env` file in the *api* folder. Then:
|
||||
|
||||
```
|
||||
npm run server
|
||||
@@ -105,7 +100,7 @@ This will install/build `web` and install `api`, then start prod server.
|
||||
|
||||
## Executables
|
||||
|
||||
Command to generate executables
|
||||
In order to generate the final executables:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -2,6 +2,68 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
|
||||
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
|
||||
|
||||
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
|
||||
|
||||
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
|
||||
|
||||
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
|
||||
|
||||
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
|
||||
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
|
||||
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
|
||||
|
||||
|
||||
|
||||
17
README.md
17
README.md
@@ -98,7 +98,20 @@ SASV9_OPTIONS= -NOXCMD
|
||||
|
||||
## Persisting the Session
|
||||
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) as follows:
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
|
||||
|
||||
1. Linux Background Job
|
||||
2. NPM package `pm2`
|
||||
|
||||
### Background Job
|
||||
|
||||
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
|
||||
|
||||
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
|
||||
|
||||
### PM2
|
||||
|
||||
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
|
||||
|
||||
```bash
|
||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||
@@ -132,7 +145,7 @@ Instead of `app_name` you can pass:
|
||||
|
||||
## Server Version
|
||||
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
|
||||
|
||||
- CLIENTID: `clientID1`
|
||||
- USERNAME: `secretuser`
|
||||
|
||||
26
api/package-lock.json
generated
26
api/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "api",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/core": "^4.19.0",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
@@ -1379,9 +1379,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
|
||||
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
|
||||
"integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
@@ -2448,9 +2448,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
@@ -11127,9 +11127,9 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/core": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
|
||||
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
|
||||
"integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
@@ -12060,9 +12060,9 @@
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"prestart": "npm run initial",
|
||||
"prebuild": "npm run initial",
|
||||
"start": "nodemon ./src/server.ts",
|
||||
"start:prod": "node ./build/src/server.js",
|
||||
"build": "rimraf build && tsc",
|
||||
"postbuild": "npm run copy:files",
|
||||
"swagger": "tsoa spec",
|
||||
@@ -46,7 +47,7 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/core": "^4.19.0",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
|
||||
@@ -418,6 +418,25 @@ components:
|
||||
example: /Public/somefolder/some.file
|
||||
type: object
|
||||
additionalProperties: false
|
||||
InfoResponse:
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
cors:
|
||||
type: string
|
||||
whiteList:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
protocol:
|
||||
type: string
|
||||
required:
|
||||
- mode
|
||||
- cors
|
||||
- whiteList
|
||||
- protocol
|
||||
type: object
|
||||
additionalProperties: false
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
@@ -1240,10 +1259,31 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||
/SASjsApi/info:
|
||||
get:
|
||||
operationId: Info
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InfoResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http}
|
||||
summary: 'Get server info (mode, cors, whiteList, protocol).'
|
||||
tags:
|
||||
- Info
|
||||
security: []
|
||||
parameters: []
|
||||
servers:
|
||||
-
|
||||
url: /
|
||||
tags:
|
||||
-
|
||||
name: Info
|
||||
description: 'Get Server Info'
|
||||
-
|
||||
name: Session
|
||||
description: 'Get Session information'
|
||||
|
||||
@@ -5,23 +5,12 @@
|
||||
_before_ any user-provided content.
|
||||
|
||||
A number of useful CORE macros are also compiled below, so that they can be
|
||||
available "out of the box".
|
||||
available by default for Stored Programs.
|
||||
|
||||
Note that the full CORE library is available to sessions in SASjs Studio.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mcf_stpsrv_header.sas
|
||||
@li mf_getuser.sas
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_mkdir.sas
|
||||
@li mf_nobs.sas
|
||||
@li mf_uid.sas
|
||||
@li mfs_httpheader.sas
|
||||
@li mp_dirlist.sas
|
||||
@li mp_ds2ddl.sas
|
||||
@li mp_ds2md.sas
|
||||
@li mp_getdbml.sas
|
||||
@li mp_init.sas
|
||||
@li mp_makedata.sas
|
||||
@li mp_zip.sas
|
||||
|
||||
@li ms_webout.sas
|
||||
**/
|
||||
|
||||
|
||||
@@ -22,11 +22,13 @@ const { MODE, CORS, WHITELIST } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||
const whiteList: string[] = []
|
||||
WHITELIST?.split(' ')?.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
WHITELIST?.split(' ')
|
||||
?.filter((url) => !!url)
|
||||
.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
|
||||
console.log('All CORS Requests are enabled for:', whiteList)
|
||||
app.use(cors({ credentials: true, origin: whiteList }))
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './group'
|
||||
export * from './session'
|
||||
export * from './stp'
|
||||
export * from './user'
|
||||
export * from './info'
|
||||
|
||||
37
api/src/controllers/info.ts
Normal file
37
api/src/controllers/info.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Route, Tags, Example, Get } from 'tsoa'
|
||||
|
||||
export interface InfoResponse {
|
||||
mode: string
|
||||
cors: string
|
||||
whiteList: string[]
|
||||
protocol: string
|
||||
}
|
||||
|
||||
@Route('SASjsApi/info')
|
||||
@Tags('Info')
|
||||
export class InfoController {
|
||||
/**
|
||||
* @summary Get server info (mode, cors, whiteList, protocol).
|
||||
*
|
||||
*/
|
||||
@Example<InfoResponse>({
|
||||
mode: 'desktop',
|
||||
cors: 'enable',
|
||||
whiteList: ['http://example.com', 'http://example2.com'],
|
||||
protocol: 'http'
|
||||
})
|
||||
@Get('/')
|
||||
public info(): InfoResponse {
|
||||
const response = {
|
||||
mode: process.env.MODE ?? 'desktop',
|
||||
cors:
|
||||
process.env.CORS ?? process.env.MODE === 'server'
|
||||
? 'disable'
|
||||
: 'enable',
|
||||
whiteList:
|
||||
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
|
||||
protocol: process.env.PROTOCOL ?? 'http'
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
verifyAdmin
|
||||
} from '../../middlewares'
|
||||
|
||||
import infoRouter from './info'
|
||||
import driveRouter from './drive'
|
||||
import stpRouter from './stp'
|
||||
import codeRouter from './code'
|
||||
@@ -20,6 +21,7 @@ import sessionRouter from './session'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/info', infoRouter)
|
||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||
router.use('/auth', desktopRestrict, authRouter)
|
||||
router.use(
|
||||
|
||||
16
api/src/routes/api/info.ts
Normal file
16
api/src/routes/api/info.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express'
|
||||
import { InfoController } from '../../controllers'
|
||||
|
||||
const infoRouter = express.Router()
|
||||
|
||||
infoRouter.get('/', async (req, res) => {
|
||||
const controller = new InfoController()
|
||||
try {
|
||||
const response = controller.info()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default infoRouter
|
||||
@@ -18,11 +18,6 @@ import {
|
||||
verifyTokenInDB
|
||||
} from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
@@ -35,12 +30,15 @@ const user = {
|
||||
}
|
||||
|
||||
describe('auth', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
await clientController.createClient({ clientId, clientSecret })
|
||||
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController, ClientController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const client = {
|
||||
clientId: 'someclientID',
|
||||
clientSecret: 'someclientSecret'
|
||||
@@ -28,12 +23,15 @@ const newClient = {
|
||||
}
|
||||
|
||||
describe('client', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
@@ -33,11 +33,6 @@ import { getTreeExample } from '../../../controllers/internal'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||
const { getTmpFilesFolderPath } = fileUtilModules
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
@@ -48,6 +43,7 @@ const user = {
|
||||
}
|
||||
|
||||
describe('drive', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const controller = new UserController()
|
||||
@@ -55,6 +51,8 @@ describe('drive', () => {
|
||||
let accessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
@@ -36,11 +31,14 @@ const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
|
||||
describe('group', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
|
||||
20
api/src/routes/api/spec/info.spec.ts
Normal file
20
api/src/routes/api/spec/info.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Express } from 'express'
|
||||
import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
|
||||
describe('Info', () => {
|
||||
let app: Express
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
})
|
||||
|
||||
it('should should return configured information of the server instance', async () => {
|
||||
const res = await request(app).get('/SASjsApi/info').expect(200)
|
||||
|
||||
expect(res.body.mode).toEqual('server')
|
||||
expect(res.body.cors).toEqual('disable')
|
||||
expect(res.body.whiteList).toEqual([])
|
||||
expect(res.body.protocol).toEqual('http')
|
||||
})
|
||||
})
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
@@ -30,10 +25,13 @@ const user = {
|
||||
const controller = new UserController()
|
||||
|
||||
describe('user', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
@@ -5,12 +5,17 @@ import { getWebBuildFolderPath } from '../../utils'
|
||||
|
||||
const webRouter = express.Router()
|
||||
|
||||
const codeToInject = `
|
||||
const jsCodeForDesktopMode = `
|
||||
<script>
|
||||
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
||||
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
||||
</script>`
|
||||
|
||||
const jsCodeForServerMode = `
|
||||
<script>
|
||||
localStorage.setItem('CLIENT_ID', '${process.env.CLIENT_ID}')
|
||||
</script>`
|
||||
|
||||
webRouter.get('/', async (_, res) => {
|
||||
let content: string
|
||||
try {
|
||||
@@ -21,14 +26,12 @@ webRouter.get('/', async (_, res) => {
|
||||
}
|
||||
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
||||
const codeToInject =
|
||||
MODE?.trim() === 'server' ? jsCodeForServerMode : jsCodeForDesktopMode
|
||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
||||
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
return res.send(injectedContent)
|
||||
}
|
||||
|
||||
return res.send(content)
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
return res.send(injectedContent)
|
||||
})
|
||||
|
||||
export default webRouter
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { populateClients } from '../routes/api/auth'
|
||||
import { seedDB } from './seedDB'
|
||||
|
||||
export const connectDB = async () => {
|
||||
// NOTE: when exporting app.js as agent for supertest
|
||||
// we should exclude connecting to the real database
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return
|
||||
} else {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
console.log('Running in Destop Mode, no DB to connect.')
|
||||
return
|
||||
}
|
||||
|
||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
||||
if (err) throw err
|
||||
|
||||
console.log('Connected to db!')
|
||||
|
||||
await populateClients()
|
||||
})
|
||||
}
|
||||
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
console.log('Running in Destop Mode, no DB to connect.')
|
||||
return
|
||||
}
|
||||
|
||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
||||
if (err) throw err
|
||||
|
||||
console.log('Connected to db!')
|
||||
|
||||
await seedDB()
|
||||
|
||||
await populateClients()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const sysInitCompiledPath = path.join(
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
|
||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||
|
||||
export const getWebBuildFolderPath = () =>
|
||||
path.join(codebaseRoot, 'web', 'build')
|
||||
|
||||
@@ -12,6 +12,7 @@ export * from './isDebugOn'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
export * from './seedDB'
|
||||
export * from './setProcessVariables'
|
||||
export * from './setupFolders'
|
||||
export * from './upload'
|
||||
|
||||
35
api/src/utils/seedDB.ts
Normal file
35
api/src/utils/seedDB.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import Client from '../model/Client'
|
||||
import User from '../model/User'
|
||||
|
||||
const CLIENT = {
|
||||
clientId: 'clientID1',
|
||||
clientSecret: 'clientSecret'
|
||||
}
|
||||
const ADMIN_USER = {
|
||||
id: 1,
|
||||
displayName: 'Super Admin',
|
||||
username: 'secretuser',
|
||||
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
export const seedDB = async () => {
|
||||
// Checking if client is already in the database
|
||||
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||
if (!clientExist) {
|
||||
const client = new Client(CLIENT)
|
||||
await client.save()
|
||||
|
||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||
}
|
||||
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||
if (!usernameExist) {
|
||||
const user = new User(ADMIN_USER)
|
||||
await user.save()
|
||||
|
||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import { getRealPath } from '@sasjs/utils'
|
||||
import { getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||
|
||||
import { configuration } from '../../package.json'
|
||||
import { getDesktopFields } from '.'
|
||||
@@ -12,18 +12,17 @@ export const setProcessVariables = async () => {
|
||||
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
if (MODE?.trim() === 'server') {
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
|
||||
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
||||
const absPath = getAbsolutePath(DRIVE_PATH ?? 'tmp', process.cwd())
|
||||
process.driveLoc = getRealPath(absPath)
|
||||
} else {
|
||||
const { sasLoc, driveLoc } = await getDesktopFields()
|
||||
|
||||
process.sasLoc = sasLoc
|
||||
process.driveLoc = driveLoc
|
||||
} else {
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
|
||||
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
||||
process.driveLoc = getRealPath(
|
||||
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
|
||||
)
|
||||
}
|
||||
|
||||
console.log('sasLoc: ', process.sasLoc)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
const usernameSchema = Joi.string().alphanum().min(6).max(20)
|
||||
const usernameSchema = Joi.string().alphanum().min(3).max(16)
|
||||
const passwordSchema = Joi.string().min(6).max(1024)
|
||||
|
||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Info",
|
||||
"description": "Get Server Info"
|
||||
},
|
||||
{
|
||||
"name": "Session",
|
||||
"description": "Get Session information"
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.51",
|
||||
"version": "0.0.58",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"version": "0.0.51",
|
||||
"version": "0.0.58",
|
||||
"devDependencies": {
|
||||
"prettier": "^2.3.1",
|
||||
"standard-version": "^9.3.2"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.51",
|
||||
"version": "0.0.58",
|
||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||
"repository": "https://github.com/sasjs/server",
|
||||
"scripts": {
|
||||
"server": "npm run server:prepare && npm run server:start",
|
||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",
|
||||
"server:start": "cd api && npm run start",
|
||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
||||
"server:start": "cd api && npm run start:prod",
|
||||
"release": "standard-version",
|
||||
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
|
||||
12
web/package-lock.json
generated
12
web/package-lock.json
generated
@@ -4191,9 +4191,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -14381,9 +14381,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { Route, HashRouter, Switch } from 'react-router-dom'
|
||||
import { ThemeProvider } from '@mui/material/styles'
|
||||
import { theme } from './theme'
|
||||
@@ -9,12 +9,12 @@ import Home from './components/home'
|
||||
import Drive from './containers/Drive'
|
||||
import Studio from './containers/Studio'
|
||||
|
||||
import useTokens from './components/useTokens'
|
||||
import { AppContext } from './context/appContext'
|
||||
|
||||
function App() {
|
||||
const { tokens, setTokens } = useTokens()
|
||||
const appContext = useContext(AppContext)
|
||||
|
||||
if (!tokens) {
|
||||
if (!appContext.tokens) {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
@@ -24,7 +24,7 @@ function App() {
|
||||
<Login getCodeOnly />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Login setTokens={setTokens} />
|
||||
<Login />
|
||||
</Route>
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
|
||||
import AppBar from '@mui/material/AppBar'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import Tabs from '@mui/material/Tabs'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import Button from '@mui/material/Button'
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from '@mui/material'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
|
||||
import UserName from './userName'
|
||||
import { AppContext } from '../context/appContext'
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
@@ -16,11 +23,29 @@ const baseUrl =
|
||||
const Header = (props: any) => {
|
||||
const history = useHistory()
|
||||
const { pathname } = useLocation()
|
||||
const appContext = useContext(AppContext)
|
||||
const [tabValue, setTabValue] = useState(pathname)
|
||||
const [anchorEl, setAnchorEl] = useState<
|
||||
(EventTarget & HTMLButtonElement) | null
|
||||
>(null)
|
||||
|
||||
const handleMenu = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
||||
setTabValue(value)
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
if (appContext.logout) appContext.logout()
|
||||
}
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
@@ -81,6 +106,39 @@ const Header = (props: any) => {
|
||||
>
|
||||
App Stream
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<UserName
|
||||
userName={appContext.userName}
|
||||
onClickHandler={handleMenu}
|
||||
/>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
open={!!anchorEl}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||
<Button variant="contained" color="primary">
|
||||
Logout
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
|
||||
import { AppContext } from '../context/appContext'
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
@@ -33,8 +34,9 @@ const getTokens = async (payload: any) => {
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
const Login = ({ getCodeOnly }: any) => {
|
||||
const location = useLocation()
|
||||
const appContext = useContext(AppContext)
|
||||
const [username, setUserName] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
@@ -45,13 +47,12 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
error = false
|
||||
setErrorMessage('')
|
||||
e.preventDefault()
|
||||
let clientId = process.env.CLIENT_ID
|
||||
let clientId = process.env.CLIENT_ID ?? localStorage.getItem('CLIENT_ID')
|
||||
|
||||
if (getCodeOnly) {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const responseType = params.get('response_type')
|
||||
if (responseType === 'code')
|
||||
clientId = params.get('client_id') ?? undefined
|
||||
if (responseType === 'code') clientId = params.get('client_id')
|
||||
}
|
||||
|
||||
const { code } = await getAuthCode({
|
||||
@@ -72,7 +73,8 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
code
|
||||
})
|
||||
|
||||
setTokens(accessToken, refreshToken)
|
||||
if (appContext.setTokens) appContext.setTokens(accessToken, refreshToken)
|
||||
if (appContext.setUserName) appContext.setUserName(username)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +129,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
required
|
||||
/>
|
||||
{errorMessage && <span>{errorMessage}</span>}
|
||||
<Button type="submit" variant="outlined">
|
||||
<Button type="submit" variant="outlined" disabled={!appContext.setTokens}>
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -135,7 +137,6 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
setTokens: PropTypes.func,
|
||||
getCodeOnly: PropTypes.bool
|
||||
}
|
||||
|
||||
|
||||
30
web/src/components/userName.tsx
Normal file
30
web/src/components/userName.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { Typography, IconButton } from '@mui/material'
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle'
|
||||
|
||||
const UserName = (props: any) => {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={props.onClickHandler}
|
||||
color="inherit"
|
||||
>
|
||||
{props.avatarContent ? (
|
||||
<img
|
||||
src={props.avatarContent}
|
||||
alt="user-avatar"
|
||||
style={{ width: '25px' }}
|
||||
/>
|
||||
) : (
|
||||
<AccountCircle></AccountCircle>
|
||||
)}
|
||||
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
|
||||
{props.userName}
|
||||
</Typography>
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserName
|
||||
@@ -90,7 +90,11 @@ const Drive = () => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
<SideBar directoryData={directoryData} handleSelect={handleSelect} />
|
||||
<SideBar
|
||||
selectedFilePath={selectedFilePath}
|
||||
directoryData={directoryData}
|
||||
handleSelect={handleSelect}
|
||||
/>
|
||||
<Main
|
||||
selectedFilePath={selectedFilePath}
|
||||
removeFileFromTree={removeFileFromTree}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { makeStyles } from '@mui/styles'
|
||||
|
||||
@@ -30,13 +30,27 @@ const useStyles = makeStyles(() => ({
|
||||
const drawerWidth = 240
|
||||
|
||||
type Props = {
|
||||
selectedFilePath: string
|
||||
directoryData: TreeNode | null
|
||||
handleSelect: (node: TreeNode) => void
|
||||
}
|
||||
|
||||
const SideBar = ({ directoryData, handleSelect }: Props) => {
|
||||
const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const defaultExpanded = useMemo(() => {
|
||||
const splittedPath = selectedFilePath.split('/')
|
||||
const arr = ['']
|
||||
let nodeId = ''
|
||||
splittedPath.forEach((path) => {
|
||||
if (path !== '') {
|
||||
nodeId += '/' + path
|
||||
arr.push(nodeId)
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}, [selectedFilePath])
|
||||
|
||||
const renderTree = (nodes: TreeNode) => (
|
||||
<TreeItem
|
||||
classes={{ root: classes.root }}
|
||||
@@ -72,7 +86,8 @@ const SideBar = ({ directoryData, handleSelect }: Props) => {
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
defaultExpanded={[directoryData.relativePath]}
|
||||
defaultExpanded={defaultExpanded}
|
||||
selected={defaultExpanded.slice(-1)}
|
||||
>
|
||||
{renderTree(directoryData)}
|
||||
</TreeView>
|
||||
|
||||
139
web/src/context/appContext.tsx
Normal file
139
web/src/context/appContext.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, {
|
||||
createContext,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||
|
||||
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
||||
|
||||
const setAxiosRequestHeader = (accessToken: string) => {
|
||||
axios.interceptors.request.use(function (config) {
|
||||
if (baseUrl && !isAbsoluteURLRegex.test(config.url as string)) {
|
||||
config.url = baseUrl + config.url
|
||||
}
|
||||
console.log('axios.interceptors.request.use', accessToken)
|
||||
config.headers!['Authorization'] = `Bearer ${accessToken}`
|
||||
config.withCredentials = true
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
const setAxiosResponse = (setTokens: Function) => {
|
||||
// Add a response interceptor
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
// Any status code that lie within the range of 2xx cause this function to trigger
|
||||
return response
|
||||
},
|
||||
async function (error) {
|
||||
if (error.response?.status === 401) {
|
||||
// refresh token
|
||||
// const { accessToken, refreshToken: newRefresh } = await refreshMyToken(
|
||||
// refreshToken
|
||||
// )
|
||||
|
||||
// if (accessToken && newRefresh) {
|
||||
// setTokens(accessToken, newRefresh)
|
||||
// error.config.headers['Authorization'] = 'Bearer ' + accessToken
|
||||
// error.config.baseURL = undefined
|
||||
|
||||
// return axios.request(error.config)
|
||||
// }
|
||||
console.log(53)
|
||||
setTokens(undefined)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getTokens = () => {
|
||||
const accessToken = localStorage.getItem('accessToken')
|
||||
const refreshToken = localStorage.getItem('refreshToken')
|
||||
|
||||
if (accessToken && refreshToken) {
|
||||
setAxiosRequestHeader(accessToken)
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
interface AppContextProps {
|
||||
userName: string
|
||||
setUserName: Dispatch<SetStateAction<string>> | null
|
||||
tokens?: { accessToken: string; refreshToken: string }
|
||||
setTokens: ((accessToken: string, refreshToken: string) => void) | null
|
||||
logout: (() => void) | null
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppContextProps>({
|
||||
userName: '',
|
||||
tokens: getTokens(),
|
||||
setUserName: null,
|
||||
setTokens: null,
|
||||
logout: null
|
||||
})
|
||||
|
||||
const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
const { children } = props
|
||||
const [userName, setUserName] = useState('')
|
||||
const [tokens, setTokens] = useState(getTokens())
|
||||
|
||||
useEffect(() => {
|
||||
setAxiosResponse(setTokens)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(97)
|
||||
if (tokens === undefined) {
|
||||
console.log(99)
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
}
|
||||
}, [tokens])
|
||||
|
||||
const saveTokens = useCallback(
|
||||
(accessToken: string, refreshToken: string) => {
|
||||
localStorage.setItem('accessToken', accessToken)
|
||||
localStorage.setItem('refreshToken', refreshToken)
|
||||
console.log(accessToken)
|
||||
setAxiosRequestHeader(accessToken)
|
||||
setTokens({ accessToken, refreshToken })
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setUserName('')
|
||||
setTokens(undefined)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
userName,
|
||||
setUserName,
|
||||
tokens,
|
||||
setTokens: saveTokens,
|
||||
logout
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppContextProvider
|
||||
@@ -2,10 +2,13 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
import AppContextProvider from './context/appContext'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<AppContextProvider>
|
||||
<App />
|
||||
</AppContextProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user