mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a932383b4 | ||
|
|
576e18347e | ||
|
|
61815f8ae1 | ||
|
|
afff27fd21 | ||
|
|
a8ba378fd1 | ||
|
|
73c81a45dc | ||
|
|
12d424acce | ||
|
|
414fb19de3 | ||
|
|
cfddf1fb0c | ||
|
|
1f483b1afc | ||
|
|
0470239ef1 | ||
|
|
2c259fe1de | ||
|
|
b066734398 | ||
|
|
3b698fce5f | ||
|
|
5ad6ee5e0f | ||
|
|
7d11cc7916 | ||
|
|
ff1def6436 | ||
|
|
c275db184e |
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,3 +1,46 @@
|
||||
## [0.3.5](https://github.com/sasjs/server/compare/v0.3.4...v0.3.5) (2022-05-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping sasjs/core library ([61815f8](https://github.com/sasjs/server/commit/61815f8ae18be132e17c199cd8e3afbcc2fa0b60))
|
||||
|
||||
## [0.3.4](https://github.com/sasjs/server/compare/v0.3.3...v0.3.4) (2022-05-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** system username for DESKTOP mode ([a8ba378](https://github.com/sasjs/server/commit/a8ba378fd1ff374ba025a96fdfae5c6c36954465))
|
||||
|
||||
## [0.3.3](https://github.com/sasjs/server/compare/v0.3.2...v0.3.3) (2022-05-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* usage of autoexec API in DESKTOP mode ([12d424a](https://github.com/sasjs/server/commit/12d424acce8108a6f53aefbac01fddcdc5efb48f))
|
||||
|
||||
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](https://github.com/sasjs/server/commit/2c259fe1de95d84e6929e311aaa6b895e66b42a3))
|
||||
|
||||
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** username should be lowercase ([5ad6ee5](https://github.com/sasjs/server/commit/5ad6ee5e0f5d7d6faa45b72215f1d9d55cfc37db))
|
||||
* **web:** reduced width for autoexec input ([7d11cc7](https://github.com/sasjs/server/commit/7d11cc79161e5a07f6c5392d742ef6b9d8658071))
|
||||
|
||||
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** added profile + edit + autoexec changes ([c275db1](https://github.com/sasjs/server/commit/c275db184e874f0ee3a4f08f2592cfacf1e90742))
|
||||
|
||||
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)
|
||||
|
||||
|
||||
|
||||
14
api/package-lock.json
generated
14
api/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "api",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.23.1",
|
||||
"@sasjs/core": "^4.27.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
@@ -1385,9 +1385,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
|
||||
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
|
||||
"version": "4.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
|
||||
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
@@ -11364,9 +11364,9 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/core": {
|
||||
"version": "4.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
|
||||
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
|
||||
"version": "4.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
|
||||
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.23.1",
|
||||
"@sasjs/core": "^4.27.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"tmp/**/*"
|
||||
"sasjs_root/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ components:
|
||||
autoExec:
|
||||
type: string
|
||||
description: 'User-specific auto-exec code'
|
||||
example: '<SAS code>'
|
||||
example: ""
|
||||
required:
|
||||
- displayName
|
||||
- username
|
||||
@@ -543,7 +543,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object}
|
||||
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
||||
loggedIn: {type: boolean}
|
||||
required:
|
||||
- user
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||
import { ExecuteReturnJsonResponse } from '.'
|
||||
import {
|
||||
getDesktopUserAutoExecPath,
|
||||
getPreProgramVariables,
|
||||
getUserAutoExec,
|
||||
ModeType,
|
||||
parseLogToArray
|
||||
} from '../utils'
|
||||
import { readFile } from '@sasjs/utils'
|
||||
|
||||
interface ExecuteSASCodePayload {
|
||||
/**
|
||||
@@ -43,7 +42,7 @@ const executeSASCode = async (
|
||||
const userAutoExec =
|
||||
process.env.MODE === ModeType.Server
|
||||
? user?.autoExec
|
||||
: await readFile(getDesktopUserAutoExecPath())
|
||||
: await getUserAutoExec()
|
||||
|
||||
try {
|
||||
const { webout, log, httpHeaders } =
|
||||
|
||||
@@ -119,9 +119,9 @@ filename _webout "${weboutPath}" mod;
|
||||
/* dynamic user-provided vars */
|
||||
${preProgramVarStatments}
|
||||
|
||||
/* user auto exec starts */
|
||||
${otherArgs?.userAutoExec}
|
||||
/* user auto exec ends */
|
||||
/* user autoexec starts */
|
||||
${otherArgs?.userAutoExec ?? ''}
|
||||
/* user autoexec ends */
|
||||
|
||||
/* actual job code */
|
||||
${program}`
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
Hidden,
|
||||
Request
|
||||
} from 'tsoa'
|
||||
import { desktopUser } from '../middlewares'
|
||||
|
||||
import User, { UserPayload } from '../model/User'
|
||||
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
|
||||
|
||||
export interface UserResponse {
|
||||
id: number
|
||||
@@ -86,6 +88,10 @@ export class UserController {
|
||||
@Request() req: express.Request,
|
||||
@Path() userId: number
|
||||
): Promise<UserDetailsResponse> {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
|
||||
|
||||
const { user } = req
|
||||
const getAutoExec = user!.isAdmin || user!.userId == userId
|
||||
return getUser(userId, getAutoExec)
|
||||
@@ -108,6 +114,11 @@ export class UserController {
|
||||
@Path() userId: number,
|
||||
@Body() body: UserPayload
|
||||
): Promise<UserDetailsResponse> {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Desktop)
|
||||
return updateDesktopAutoExec(body.autoExec ?? '')
|
||||
|
||||
return updateUser(userId, body)
|
||||
}
|
||||
|
||||
@@ -177,7 +188,15 @@ const getUser = async (
|
||||
username: user.username,
|
||||
isActive: user.isActive,
|
||||
isAdmin: user.isAdmin,
|
||||
autoExec: getAutoExec ? user.autoExec : undefined
|
||||
autoExec: getAutoExec ? user.autoExec ?? '' : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getDesktopAutoExec = async () => {
|
||||
return {
|
||||
...desktopUser,
|
||||
id: desktopUser.userId,
|
||||
autoExec: await getUserAutoExec()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +235,15 @@ const updateUser = async (
|
||||
}
|
||||
}
|
||||
|
||||
const updateDesktopAutoExec = async (autoExec: string) => {
|
||||
await updateUserAutoExec(autoExec)
|
||||
return {
|
||||
...desktopUser,
|
||||
id: desktopUser.userId,
|
||||
autoExec
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = async (
|
||||
id: number,
|
||||
isAdmin: boolean,
|
||||
|
||||
@@ -97,6 +97,7 @@ const login = async (
|
||||
return {
|
||||
loggedIn: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { csrfProtection } from '../app'
|
||||
import { fetchLatestAutoExec, verifyTokenInDB } from '../utils'
|
||||
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
||||
import { desktopUser } from './desktop'
|
||||
|
||||
export const authenticateAccessToken: RequestHandler = async (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE === ModeType.Desktop) {
|
||||
req.user = desktopUser
|
||||
return next()
|
||||
}
|
||||
|
||||
// if request is coming from web and has valid session
|
||||
// we can validate the request and check for CSRF Token
|
||||
// it can be validated.
|
||||
if (req.session?.loggedIn) {
|
||||
if (req.session.user) {
|
||||
const user = await fetchLatestAutoExec(req.session.user)
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import { RequestHandler } from 'express'
|
||||
import { RequestHandler, Request } from 'express'
|
||||
import { userInfo } from 'os'
|
||||
import { RequestUser } from '../types'
|
||||
import { ModeType } from '../utils'
|
||||
|
||||
const regexUser = /^\/SASjsApi\/user\/[0-9]*$/ // /SASjsApi/user/1
|
||||
|
||||
const allowedInDesktopMode: { [key: string]: RegExp[] } = {
|
||||
GET: [regexUser],
|
||||
PATCH: [regexUser]
|
||||
}
|
||||
|
||||
const reqAllowedInDesktopMode = (request: Request): boolean => {
|
||||
const { method, originalUrl: url } = request
|
||||
|
||||
return !!allowedInDesktopMode[method]?.find((urlRegex) => urlRegex.test(url))
|
||||
}
|
||||
|
||||
export const desktopRestrict: RequestHandler = (req, res, next) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server')
|
||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||
|
||||
if (MODE === ModeType.Desktop) {
|
||||
if (!reqAllowedInDesktopMode(req))
|
||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
export const desktopUsername: RequestHandler = (req, res, next) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server')
|
||||
return res.status(200).send({
|
||||
userId: 12345,
|
||||
username: 'DESKTOPusername',
|
||||
displayName: 'DESKTOP User'
|
||||
})
|
||||
|
||||
next()
|
||||
export const desktopUser: RequestUser = {
|
||||
userId: 12345,
|
||||
clientId: 'desktop_app',
|
||||
username: userInfo().username,
|
||||
displayName: userInfo().username,
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface UserPayload {
|
||||
isActive?: boolean
|
||||
/**
|
||||
* User-specific auto-exec code
|
||||
* @example "<SAS code>"
|
||||
* @example ""
|
||||
*/
|
||||
autoExec?: string
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import swaggerUi from 'swagger-ui-express'
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
desktopRestrict,
|
||||
desktopUsername,
|
||||
verifyAdmin
|
||||
} from '../../middlewares'
|
||||
|
||||
@@ -22,7 +21,7 @@ import sessionRouter from './session'
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/info', infoRouter)
|
||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||
router.use('/session', authenticateAccessToken, sessionRouter)
|
||||
router.use('/auth', desktopRestrict, authRouter)
|
||||
router.use(
|
||||
'/client',
|
||||
|
||||
@@ -9,14 +9,14 @@ import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
username: 'testAdminUsername',
|
||||
username: 'testadminusername',
|
||||
password: '12345678',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
username: 'testusername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true,
|
||||
@@ -68,6 +68,20 @@ describe('user', () => {
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with new user having username as lowercase', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ ...user, username: user.username.toUpperCase() })
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
@@ -244,7 +258,7 @@ describe('user', () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
username: 'randomuser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
|
||||
@@ -10,7 +10,7 @@ const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
id: 1234,
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
username: 'testusername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
@@ -77,6 +77,7 @@ describe('web', () => {
|
||||
|
||||
expect(res.body.loggedIn).toBeTruthy()
|
||||
expect(res.body.user).toEqual({
|
||||
id: expect.any(Number),
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
})
|
||||
@@ -155,7 +156,6 @@ const getCSRF = async (app: Express) => {
|
||||
const { header } = await request(app).get('/')
|
||||
const cookies = header['set-cookie'].join()
|
||||
|
||||
console.log('cookies', cookies)
|
||||
const csrfToken = extractCSRF(cookies)
|
||||
return { csrfToken, cookies }
|
||||
}
|
||||
|
||||
@@ -47,10 +47,11 @@ stpRouter.post(
|
||||
query?._program
|
||||
)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
return res.end(response)
|
||||
}
|
||||
// TODO: investigate if this code is required
|
||||
// if (response instanceof Buffer) {
|
||||
// res.writeHead(200, (req as any).sasHeaders)
|
||||
// return res.end(response)
|
||||
// }
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express'
|
||||
import { WebController } from '../../controllers/web'
|
||||
import { authenticateAccessToken } from '../../middlewares'
|
||||
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
|
||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||
|
||||
const webRouter = express.Router()
|
||||
@@ -19,7 +19,7 @@ webRouter.get('/', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
webRouter.post('/SASLogon/login', async (req, res) => {
|
||||
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
|
||||
const { error, value: body } = loginWebValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
@@ -33,6 +33,7 @@ webRouter.post('/SASLogon/login', async (req, res) => {
|
||||
|
||||
webRouter.post(
|
||||
'/SASLogon/authorize',
|
||||
desktopRestrict,
|
||||
authenticateAccessToken,
|
||||
async (req, res) => {
|
||||
const { error, value: body } = authorizeValidation(req.body)
|
||||
@@ -47,7 +48,7 @@ webRouter.post(
|
||||
}
|
||||
)
|
||||
|
||||
webRouter.get('/logout', async (req, res) => {
|
||||
webRouter.get('/logout', desktopRestrict, async (req, res) => {
|
||||
try {
|
||||
await controller.logout(req)
|
||||
res.status(200).send('OK!')
|
||||
|
||||
8
api/src/utils/desktopAutoExec.ts
Normal file
8
api/src/utils/desktopAutoExec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createFile, readFile } from '@sasjs/utils'
|
||||
import { getDesktopUserAutoExecPath } from './file'
|
||||
|
||||
export const getUserAutoExec = async (): Promise<string> =>
|
||||
readFile(getDesktopUserAutoExecPath())
|
||||
|
||||
export const updateUserAutoExec = async (autoExecContent: string) =>
|
||||
createFile(getDesktopUserAutoExecPath(), autoExecContent)
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './appStreamConfig'
|
||||
export * from './connectDB'
|
||||
export * from './copySASjsCore'
|
||||
export * from './desktopAutoExec'
|
||||
export * from './extractHeaders'
|
||||
export * from './file'
|
||||
export * from './generateAccessToken'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
const usernameSchema = Joi.string().alphanum().min(3).max(16)
|
||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||
const passwordSchema = Joi.string().min(6).max(1024)
|
||||
|
||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||
@@ -36,7 +36,7 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
|
||||
password: passwordSchema.required(),
|
||||
isAdmin: Joi.boolean(),
|
||||
isActive: Joi.boolean(),
|
||||
autoExec: Joi.string()
|
||||
autoExec: Joi.string().allow('')
|
||||
}).validate(data)
|
||||
|
||||
export const deleteUserValidation = (
|
||||
@@ -59,7 +59,7 @@ export const updateUserValidation = (
|
||||
displayName: Joi.string().min(6),
|
||||
username: usernameSchema,
|
||||
password: passwordSchema,
|
||||
autoExec: Joi.string()
|
||||
autoExec: Joi.string().allow('')
|
||||
}
|
||||
if (isAdmin) {
|
||||
validationChecks.isAdmin = Joi.boolean()
|
||||
|
||||
@@ -8,9 +8,11 @@ import Header from './components/header'
|
||||
import Home from './components/home'
|
||||
import Drive from './containers/Drive'
|
||||
import Studio from './containers/Studio'
|
||||
import Settings from './containers/Settings'
|
||||
|
||||
import { AppContext } from './context/appContext'
|
||||
import AuthCode from './containers/AuthCode'
|
||||
import { ToastContainer } from 'react-toastify'
|
||||
|
||||
function App() {
|
||||
const appContext = useContext(AppContext)
|
||||
@@ -44,10 +46,14 @@ function App() {
|
||||
<Route exact path="/SASjsStudio">
|
||||
<Studio />
|
||||
</Route>
|
||||
<Route exact path="/SASjsSettings">
|
||||
<Settings />
|
||||
</Route>
|
||||
<Route exact path="/SASjsLogon">
|
||||
<AuthCode />
|
||||
</Route>
|
||||
</Switch>
|
||||
<ToastContainer />
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useContext } from 'react'
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MenuItem
|
||||
} from '@mui/material'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
import SettingsIcon from '@mui/icons-material/Settings'
|
||||
|
||||
import Username from './username'
|
||||
import { AppContext } from '../context/appContext'
|
||||
@@ -20,17 +21,23 @@ const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||
|
||||
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
||||
|
||||
const Header = (props: any) => {
|
||||
const history = useHistory()
|
||||
const { pathname } = useLocation()
|
||||
const appContext = useContext(AppContext)
|
||||
const [tabValue, setTabValue] = useState(
|
||||
pathname === '/SASjsLogon' ? '/' : pathname
|
||||
validTabs.includes(pathname) ? pathname : '/'
|
||||
)
|
||||
const [anchorEl, setAnchorEl] = useState<
|
||||
(EventTarget & HTMLButtonElement) | null
|
||||
>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
||||
}, [pathname])
|
||||
|
||||
const handleMenu = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
@@ -46,7 +53,10 @@ const Header = (props: any) => {
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
if (appContext.logout) appContext.logout()
|
||||
if (appContext.logout) {
|
||||
handleClose()
|
||||
appContext.logout()
|
||||
}
|
||||
}
|
||||
return (
|
||||
<AppBar
|
||||
@@ -134,6 +144,18 @@ const Header = (props: any) => {
|
||||
open={!!anchorEl}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/SASjsSettings"
|
||||
onClick={handleClose}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||
<Button variant="contained" color="primary">
|
||||
Logout
|
||||
|
||||
@@ -27,9 +27,10 @@ const Login = () => {
|
||||
})
|
||||
|
||||
if (loggedIn) {
|
||||
appContext.setLoggedIn?.(loggedIn)
|
||||
appContext.setUserId?.(user.id)
|
||||
appContext.setUsername?.(user.username)
|
||||
appContext.setDisplayName?.(user.displayName)
|
||||
appContext.setLoggedIn?.(loggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ToastContainer, toast } from 'react-toastify'
|
||||
import { toast } from 'react-toastify'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
@@ -71,8 +71,6 @@ const AuthCode = () => {
|
||||
>
|
||||
<Button variant="contained">Copy to Clipboard</Button>
|
||||
</CopyToClipboard>
|
||||
|
||||
<ToastContainer />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
55
web/src/containers/Settings/index.tsx
Normal file
55
web/src/containers/Settings/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { Box, Paper, Tab, styled } from '@mui/material'
|
||||
import TabContext from '@mui/lab/TabContext'
|
||||
import TabList from '@mui/lab/TabList'
|
||||
import TabPanel from '@mui/lab/TabPanel'
|
||||
|
||||
import Profile from './profile'
|
||||
|
||||
const StyledTab = styled(Tab)({
|
||||
background: 'black',
|
||||
margin: '0 5px 5px 0'
|
||||
})
|
||||
|
||||
const StyledTabpanel = styled(TabPanel)({
|
||||
flexGrow: 1
|
||||
})
|
||||
|
||||
const Settings = () => {
|
||||
const [value, setValue] = React.useState('profile')
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
marginTop: '65px'
|
||||
}}
|
||||
>
|
||||
<TabContext value={value}>
|
||||
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
|
||||
<TabList
|
||||
TabIndicatorProps={{
|
||||
style: {
|
||||
display: 'none'
|
||||
}
|
||||
}}
|
||||
orientation="vertical"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<StyledTab label="Profile" value="profile" />
|
||||
</TabList>
|
||||
</Box>
|
||||
<StyledTabpanel value="profile">
|
||||
<Profile />
|
||||
</StyledTabpanel>
|
||||
</TabContext>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
||||
150
web/src/containers/Settings/profile.tsx
Normal file
150
web/src/containers/Settings/profile.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Grid,
|
||||
CircularProgress,
|
||||
Card,
|
||||
CardHeader,
|
||||
Divider,
|
||||
CardContent,
|
||||
TextField,
|
||||
CardActions,
|
||||
Button,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Checkbox
|
||||
} from '@mui/material'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { AppContext, ModeType } from '../../context/appContext'
|
||||
|
||||
const Profile = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const appContext = useContext(AppContext)
|
||||
const [user, setUser] = useState({} as any)
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.get(`/SASjsApi/user/${appContext.userId}`)
|
||||
.then((res: any) => {
|
||||
setUser(res.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleChange = (event: any) => {
|
||||
const { name, value } = event.target
|
||||
|
||||
setUser({ ...user, [name]: value })
|
||||
}
|
||||
const handleSubmit = () => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.patch(`/SASjsApi/user/${appContext.userId}`, {
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
autoExec: user.autoExec
|
||||
})
|
||||
.then((res: any) => {
|
||||
toast.success('User information updated', {
|
||||
theme: 'dark',
|
||||
position: toast.POSITION.BOTTOM_RIGHT
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed: ' + err.response?.data || err.text, {
|
||||
theme: 'dark',
|
||||
position: toast.POSITION.BOTTOM_RIGHT
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<CircularProgress
|
||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader title="Profile Information" />
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item md={6} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
error={user.displayName?.length === 0}
|
||||
helperText="Please specify display name"
|
||||
label="Display Name"
|
||||
name="displayName"
|
||||
onChange={handleChange}
|
||||
required
|
||||
value={user.displayName}
|
||||
variant="outlined"
|
||||
disabled={appContext.mode === ModeType.Desktop}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
error={user.username?.length === 0}
|
||||
helperText="Please specify username"
|
||||
label="Username"
|
||||
name="username"
|
||||
onChange={handleChange}
|
||||
required
|
||||
value={user.username}
|
||||
variant="outlined"
|
||||
disabled={appContext.mode === ModeType.Desktop}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item lg={6} md={8} sm={12} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="autoExec"
|
||||
name="autoExec"
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
rows="10"
|
||||
value={user.autoExec}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<FormGroup row>
|
||||
<FormControlLabel
|
||||
disabled
|
||||
control={<Checkbox checked={user.isActive} />}
|
||||
label="isActive"
|
||||
/>
|
||||
<FormControlLabel
|
||||
disabled
|
||||
control={<Checkbox checked={user.isAdmin} />}
|
||||
label="isAdmin"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions>
|
||||
<Button type="submit" variant="contained" onClick={handleSubmit}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Profile
|
||||
@@ -9,14 +9,22 @@ import React, {
|
||||
} from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
export enum ModeType {
|
||||
Server = 'server',
|
||||
Desktop = 'desktop'
|
||||
}
|
||||
|
||||
interface AppContextProps {
|
||||
checkingSession: boolean
|
||||
loggedIn: boolean
|
||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||
userId: number
|
||||
setUserId: Dispatch<SetStateAction<number>> | null
|
||||
username: string
|
||||
setUsername: Dispatch<SetStateAction<string>> | null
|
||||
displayName: string
|
||||
setDisplayName: Dispatch<SetStateAction<string>> | null
|
||||
mode: ModeType
|
||||
logout: (() => void) | null
|
||||
}
|
||||
|
||||
@@ -24,10 +32,13 @@ export const AppContext = createContext<AppContextProps>({
|
||||
checkingSession: false,
|
||||
loggedIn: false,
|
||||
setLoggedIn: null,
|
||||
userId: 0,
|
||||
setUserId: null,
|
||||
username: '',
|
||||
setUsername: null,
|
||||
displayName: '',
|
||||
setDisplayName: null,
|
||||
mode: ModeType.Server,
|
||||
logout: null
|
||||
})
|
||||
|
||||
@@ -35,8 +46,10 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
const { children } = props
|
||||
const [checkingSession, setCheckingSession] = useState(false)
|
||||
const [loggedIn, setLoggedIn] = useState(false)
|
||||
const [userId, setUserId] = useState(0)
|
||||
const [username, setUsername] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
const [mode, setMode] = useState(ModeType.Server)
|
||||
|
||||
useEffect(() => {
|
||||
setCheckingSession(true)
|
||||
@@ -46,14 +59,23 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
.then((res) => res.data)
|
||||
.then((data: any) => {
|
||||
setCheckingSession(false)
|
||||
setLoggedIn(true)
|
||||
setUserId(data.id)
|
||||
setUsername(data.username)
|
||||
setDisplayName(data.displayName)
|
||||
setLoggedIn(true)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoggedIn(false)
|
||||
axios.get('/') // get CSRF TOKEN
|
||||
})
|
||||
|
||||
axios
|
||||
.get('/SASjsApi/info')
|
||||
.then((res) => res.data)
|
||||
.then((data: any) => {
|
||||
setMode(data.mode)
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(() => {
|
||||
@@ -70,10 +92,13 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
checkingSession,
|
||||
loggedIn,
|
||||
setLoggedIn,
|
||||
userId,
|
||||
setUserId,
|
||||
username,
|
||||
setUsername,
|
||||
displayName,
|
||||
setDisplayName,
|
||||
mode,
|
||||
logout
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user