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

Compare commits

...

15 Commits

Author SHA1 Message Date
semantic-release-bot
afff27fd21 chore(release): 0.3.4 [skip ci]
## [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](a8ba378fd1))
2022-05-30 16:12:22 +00:00
Saad Jutt
a8ba378fd1 fix(web): system username for DESKTOP mode 2022-05-30 21:08:17 +05:00
semantic-release-bot
73c81a45dc chore(release): 0.3.3 [skip ci]
## [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](12d424acce))
2022-05-30 12:18:45 +00:00
Saad Jutt
12d424acce fix: usage of autoexec API in DESKTOP mode 2022-05-30 17:12:17 +05:00
Saad Jutt
414fb19de3 chore: code changes 2022-05-30 00:32:05 +05:00
semantic-release-bot
cfddf1fb0c chore(release): 0.3.2 [skip ci]
## [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](2c259fe1de))
2022-05-27 19:43:00 +00:00
Muhammad Saad
1f483b1afc Merge pull request #180 from sasjs/desktop-autoexec
fix(web): ability to use get/patch User API in desktop mode.
2022-05-27 12:39:17 -07:00
Saad Jutt
0470239ef1 chore: quick fix 2022-05-27 17:35:58 +05:00
Saad Jutt
2c259fe1de fix(web): ability to use get/patch User API in desktop mode. 2022-05-27 17:01:14 +05:00
semantic-release-bot
b066734398 chore(release): 0.3.1 [skip ci]
## [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](5ad6ee5e0f))
* **web:** reduced width for autoexec input ([7d11cc7](7d11cc7916))
2022-05-26 15:30:45 +00:00
Allan Bowe
3b698fce5f Merge pull request #179 from sasjs/web-profile-fixes
Web profile fixes
2022-05-26 18:26:30 +03:00
Saad Jutt
5ad6ee5e0f fix(api): username should be lowercase 2022-05-26 20:20:02 +05:00
Saad Jutt
7d11cc7916 fix(web): reduced width for autoexec input 2022-05-26 19:48:59 +05:00
semantic-release-bot
ff1def6436 chore(release): 0.3.0 [skip ci]
# [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](c275db184e))
2022-05-25 23:29:22 +00:00
Saad Jutt
c275db184e feat(web): added profile + edit + autoexec changes 2022-05-26 04:25:15 +05:00
25 changed files with 419 additions and 50 deletions

View File

@@ -1,3 +1,39 @@
## [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) # [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)

View File

@@ -96,7 +96,7 @@
}, },
"nodemonConfig": { "nodemonConfig": {
"ignore": [ "ignore": [
"tmp/**/*" "sasjs_root/**/*"
] ]
} }
} }

View File

@@ -357,7 +357,7 @@ components:
autoExec: autoExec:
type: string type: string
description: 'User-specific auto-exec code' description: 'User-specific auto-exec code'
example: '<SAS code>' example: ""
required: required:
- displayName - displayName
- username - username
@@ -543,7 +543,7 @@ paths:
application/json: application/json:
schema: schema:
properties: 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} loggedIn: {type: boolean}
required: required:
- user - user

View File

@@ -3,12 +3,11 @@ import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal' import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.' import { ExecuteReturnJsonResponse } from '.'
import { import {
getDesktopUserAutoExecPath,
getPreProgramVariables, getPreProgramVariables,
getUserAutoExec,
ModeType, ModeType,
parseLogToArray parseLogToArray
} from '../utils' } from '../utils'
import { readFile } from '@sasjs/utils'
interface ExecuteSASCodePayload { interface ExecuteSASCodePayload {
/** /**
@@ -43,7 +42,7 @@ const executeSASCode = async (
const userAutoExec = const userAutoExec =
process.env.MODE === ModeType.Server process.env.MODE === ModeType.Server
? user?.autoExec ? user?.autoExec
: await readFile(getDesktopUserAutoExecPath()) : await getUserAutoExec()
try { try {
const { webout, log, httpHeaders } = const { webout, log, httpHeaders } =

View File

@@ -119,9 +119,9 @@ filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */ /* dynamic user-provided vars */
${preProgramVarStatments} ${preProgramVarStatments}
/* user auto exec starts */ /* user autoexec starts */
${otherArgs?.userAutoExec} ${otherArgs?.userAutoExec ?? ''}
/* user auto exec ends */ /* user autoexec ends */
/* actual job code */ /* actual job code */
${program}` ${program}`

View File

@@ -14,8 +14,10 @@ import {
Hidden, Hidden,
Request Request
} from 'tsoa' } from 'tsoa'
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User' import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
export interface UserResponse { export interface UserResponse {
id: number id: number
@@ -86,6 +88,10 @@ export class UserController {
@Request() req: express.Request, @Request() req: express.Request,
@Path() userId: number @Path() userId: number
): Promise<UserDetailsResponse> { ): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req const { user } = req
const getAutoExec = user!.isAdmin || user!.userId == userId const getAutoExec = user!.isAdmin || user!.userId == userId
return getUser(userId, getAutoExec) return getUser(userId, getAutoExec)
@@ -108,6 +114,11 @@ export class UserController {
@Path() userId: number, @Path() userId: number,
@Body() body: UserPayload @Body() body: UserPayload
): Promise<UserDetailsResponse> { ): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser(userId, body) return updateUser(userId, body)
} }
@@ -177,7 +188,15 @@ const getUser = async (
username: user.username, username: user.username,
isActive: user.isActive, isActive: user.isActive,
isAdmin: user.isAdmin, 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 ( const deleteUser = async (
id: number, id: number,
isAdmin: boolean, isAdmin: boolean,

View File

@@ -97,6 +97,7 @@ const login = async (
return { return {
loggedIn: true, loggedIn: true,
user: { user: {
id: user.id,
username: user.username, username: user.username,
displayName: user.displayName displayName: user.displayName
} }

View File

@@ -1,15 +1,22 @@
import { RequestHandler, Request, Response, NextFunction } from 'express' import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app' import { csrfProtection } from '../app'
import { fetchLatestAutoExec, verifyTokenInDB } from '../utils' import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
import { desktopUser } from './desktop'
export const authenticateAccessToken: RequestHandler = async ( export const authenticateAccessToken: RequestHandler = async (
req, req,
res, res,
next next
) => { ) => {
const { MODE } = process.env
if (MODE === ModeType.Desktop) {
req.user = desktopUser
return next()
}
// if request is coming from web and has valid session // 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?.loggedIn) {
if (req.session.user) { if (req.session.user) {
const user = await fetchLatestAutoExec(req.session.user) const user = await fetchLatestAutoExec(req.session.user)

View File

@@ -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) => { export const desktopRestrict: RequestHandler = (req, res, next) => {
const { MODE } = process.env const { MODE } = process.env
if (MODE?.trim() !== 'server')
if (MODE === ModeType.Desktop) {
if (!reqAllowedInDesktopMode(req))
return res.status(403).send('Not Allowed while in Desktop Mode.') return res.status(403).send('Not Allowed while in Desktop Mode.')
}
next() next()
} }
export const desktopUsername: RequestHandler = (req, res, next) => {
const { MODE } = process.env export const desktopUser: RequestUser = {
if (MODE?.trim() !== 'server')
return res.status(200).send({
userId: 12345, userId: 12345,
username: 'DESKTOPusername', clientId: 'desktop_app',
displayName: 'DESKTOP User' username: userInfo().username,
}) displayName: userInfo().username,
isAdmin: true,
next() isActive: true
} }

View File

@@ -29,7 +29,7 @@ export interface UserPayload {
isActive?: boolean isActive?: boolean
/** /**
* User-specific auto-exec code * User-specific auto-exec code
* @example "<SAS code>" * @example ""
*/ */
autoExec?: string autoExec?: string
} }

View File

@@ -5,7 +5,6 @@ import swaggerUi from 'swagger-ui-express'
import { import {
authenticateAccessToken, authenticateAccessToken,
desktopRestrict, desktopRestrict,
desktopUsername,
verifyAdmin verifyAdmin
} from '../../middlewares' } from '../../middlewares'
@@ -22,7 +21,7 @@ import sessionRouter from './session'
const router = express.Router() const router = express.Router()
router.use('/info', infoRouter) router.use('/info', infoRouter)
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter) router.use('/session', authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter) router.use('/auth', desktopRestrict, authRouter)
router.use( router.use(
'/client', '/client',

View File

@@ -9,14 +9,14 @@ import { generateAccessToken, saveTokensInDB } from '../../../utils'
const clientId = 'someclientID' const clientId = 'someclientID'
const adminUser = { const adminUser = {
displayName: 'Test Admin', displayName: 'Test Admin',
username: 'testAdminUsername', username: 'testadminusername',
password: '12345678', password: '12345678',
isAdmin: true, isAdmin: true,
isActive: true isActive: true
} }
const user = { const user = {
displayName: 'Test User', displayName: 'Test User',
username: 'testUsername', username: 'testusername',
password: '87654321', password: '87654321',
isAdmin: false, isAdmin: false,
isActive: true, isActive: true,
@@ -68,6 +68,20 @@ describe('user', () => {
expect(res.body.autoExec).toEqual(user.autoExec) 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 () => { it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app) const res = await request(app)
.post('/SASjsApi/user') .post('/SASjsApi/user')
@@ -244,7 +258,7 @@ describe('user', () => {
const dbUser1 = await controller.createUser(user) const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({ const dbUser2 = await controller.createUser({
...user, ...user,
username: 'randomUser' username: 'randomuser'
}) })
const res = await request(app) const res = await request(app)

View File

@@ -10,7 +10,7 @@ const clientSecret = 'someclientSecret'
const user = { const user = {
id: 1234, id: 1234,
displayName: 'Test User', displayName: 'Test User',
username: 'testUsername', username: 'testusername',
password: '87654321', password: '87654321',
isAdmin: false, isAdmin: false,
isActive: true isActive: true
@@ -77,6 +77,7 @@ describe('web', () => {
expect(res.body.loggedIn).toBeTruthy() expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({ expect(res.body.user).toEqual({
id: expect.any(Number),
username: user.username, username: user.username,
displayName: user.displayName displayName: user.displayName
}) })
@@ -155,7 +156,6 @@ const getCSRF = async (app: Express) => {
const { header } = await request(app).get('/') const { header } = await request(app).get('/')
const cookies = header['set-cookie'].join() const cookies = header['set-cookie'].join()
console.log('cookies', cookies)
const csrfToken = extractCSRF(cookies) const csrfToken = extractCSRF(cookies)
return { csrfToken, cookies } return { csrfToken, cookies }
} }

View File

@@ -47,10 +47,11 @@ stpRouter.post(
query?._program query?._program
) )
if (response instanceof Buffer) { // TODO: investigate if this code is required
res.writeHead(200, (req as any).sasHeaders) // if (response instanceof Buffer) {
return res.end(response) // res.writeHead(200, (req as any).sasHeaders)
} // return res.end(response)
// }
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {

View File

@@ -1,6 +1,6 @@
import express from 'express' import express from 'express'
import { WebController } from '../../controllers/web' import { WebController } from '../../controllers/web'
import { authenticateAccessToken } from '../../middlewares' import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils' import { authorizeValidation, loginWebValidation } from '../../utils'
const webRouter = express.Router() 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) const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message) if (error) return res.status(400).send(error.details[0].message)
@@ -33,6 +33,7 @@ webRouter.post('/SASLogon/login', async (req, res) => {
webRouter.post( webRouter.post(
'/SASLogon/authorize', '/SASLogon/authorize',
desktopRestrict,
authenticateAccessToken, authenticateAccessToken,
async (req, res) => { async (req, res) => {
const { error, value: body } = authorizeValidation(req.body) 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 { try {
await controller.logout(req) await controller.logout(req)
res.status(200).send('OK!') res.status(200).send('OK!')

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

View File

@@ -1,6 +1,7 @@
export * from './appStreamConfig' export * from './appStreamConfig'
export * from './connectDB' export * from './connectDB'
export * from './copySASjsCore' export * from './copySASjsCore'
export * from './desktopAutoExec'
export * from './extractHeaders' export * from './extractHeaders'
export * from './file' export * from './file'
export * from './generateAccessToken' export * from './generateAccessToken'

View File

@@ -1,6 +1,6 @@
import Joi from 'joi' 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) const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i export const blockFileRegex = /\.(exe|sh|htaccess)$/i
@@ -36,7 +36,7 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
password: passwordSchema.required(), password: passwordSchema.required(),
isAdmin: Joi.boolean(), isAdmin: Joi.boolean(),
isActive: Joi.boolean(), isActive: Joi.boolean(),
autoExec: Joi.string() autoExec: Joi.string().allow('')
}).validate(data) }).validate(data)
export const deleteUserValidation = ( export const deleteUserValidation = (
@@ -59,7 +59,7 @@ export const updateUserValidation = (
displayName: Joi.string().min(6), displayName: Joi.string().min(6),
username: usernameSchema, username: usernameSchema,
password: passwordSchema, password: passwordSchema,
autoExec: Joi.string() autoExec: Joi.string().allow('')
} }
if (isAdmin) { if (isAdmin) {
validationChecks.isAdmin = Joi.boolean() validationChecks.isAdmin = Joi.boolean()

View File

@@ -8,9 +8,11 @@ import Header from './components/header'
import Home from './components/home' import Home from './components/home'
import Drive from './containers/Drive' import Drive from './containers/Drive'
import Studio from './containers/Studio' import Studio from './containers/Studio'
import Settings from './containers/Settings'
import { AppContext } from './context/appContext' import { AppContext } from './context/appContext'
import AuthCode from './containers/AuthCode' import AuthCode from './containers/AuthCode'
import { ToastContainer } from 'react-toastify'
function App() { function App() {
const appContext = useContext(AppContext) const appContext = useContext(AppContext)
@@ -44,10 +46,14 @@ function App() {
<Route exact path="/SASjsStudio"> <Route exact path="/SASjsStudio">
<Studio /> <Studio />
</Route> </Route>
<Route exact path="/SASjsSettings">
<Settings />
</Route>
<Route exact path="/SASjsLogon"> <Route exact path="/SASjsLogon">
<AuthCode /> <AuthCode />
</Route> </Route>
</Switch> </Switch>
<ToastContainer />
</HashRouter> </HashRouter>
</ThemeProvider> </ThemeProvider>
) )

View File

@@ -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 { Link, useHistory, useLocation } from 'react-router-dom'
import { import {
@@ -11,6 +11,7 @@ import {
MenuItem MenuItem
} from '@mui/material' } from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew' import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import SettingsIcon from '@mui/icons-material/Settings'
import Username from './username' import Username from './username'
import { AppContext } from '../context/appContext' import { AppContext } from '../context/appContext'
@@ -20,17 +21,23 @@ const PORT_API = process.env.PORT_API
const baseUrl = const baseUrl =
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : '' NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
const Header = (props: any) => { const Header = (props: any) => {
const history = useHistory() const history = useHistory()
const { pathname } = useLocation() const { pathname } = useLocation()
const appContext = useContext(AppContext) const appContext = useContext(AppContext)
const [tabValue, setTabValue] = useState( const [tabValue, setTabValue] = useState(
pathname === '/SASjsLogon' ? '/' : pathname validTabs.includes(pathname) ? pathname : '/'
) )
const [anchorEl, setAnchorEl] = useState< const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null (EventTarget & HTMLButtonElement) | null
>(null) >(null)
useEffect(() => {
setTabValue(validTabs.includes(pathname) ? pathname : '/')
}, [pathname])
const handleMenu = ( const handleMenu = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => { ) => {
@@ -46,7 +53,10 @@ const Header = (props: any) => {
} }
const handleLogout = () => { const handleLogout = () => {
if (appContext.logout) appContext.logout() if (appContext.logout) {
handleClose()
appContext.logout()
}
} }
return ( return (
<AppBar <AppBar
@@ -134,6 +144,18 @@ const Header = (props: any) => {
open={!!anchorEl} open={!!anchorEl}
onClose={handleClose} 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' }}> <MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
<Button variant="contained" color="primary"> <Button variant="contained" color="primary">
Logout Logout

View File

@@ -27,9 +27,10 @@ const Login = () => {
}) })
if (loggedIn) { if (loggedIn) {
appContext.setLoggedIn?.(loggedIn) appContext.setUserId?.(user.id)
appContext.setUsername?.(user.username) appContext.setUsername?.(user.username)
appContext.setDisplayName?.(user.displayName) appContext.setDisplayName?.(user.displayName)
appContext.setLoggedIn?.(loggedIn)
} }
} }

View File

@@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
import { CopyToClipboard } from 'react-copy-to-clipboard' import { CopyToClipboard } from 'react-copy-to-clipboard'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { ToastContainer, toast } from 'react-toastify' import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css' import 'react-toastify/dist/ReactToastify.css'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
@@ -71,8 +71,6 @@ const AuthCode = () => {
> >
<Button variant="contained">Copy to Clipboard</Button> <Button variant="contained">Copy to Clipboard</Button>
</CopyToClipboard> </CopyToClipboard>
<ToastContainer />
</Box> </Box>
) )
} }

View 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

View 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

View File

@@ -9,14 +9,22 @@ import React, {
} from 'react' } from 'react'
import axios from 'axios' import axios from 'axios'
export enum ModeType {
Server = 'server',
Desktop = 'desktop'
}
interface AppContextProps { interface AppContextProps {
checkingSession: boolean checkingSession: boolean
loggedIn: boolean loggedIn: boolean
setLoggedIn: Dispatch<SetStateAction<boolean>> | null setLoggedIn: Dispatch<SetStateAction<boolean>> | null
userId: number
setUserId: Dispatch<SetStateAction<number>> | null
username: string username: string
setUsername: Dispatch<SetStateAction<string>> | null setUsername: Dispatch<SetStateAction<string>> | null
displayName: string displayName: string
setDisplayName: Dispatch<SetStateAction<string>> | null setDisplayName: Dispatch<SetStateAction<string>> | null
mode: ModeType
logout: (() => void) | null logout: (() => void) | null
} }
@@ -24,10 +32,13 @@ export const AppContext = createContext<AppContextProps>({
checkingSession: false, checkingSession: false,
loggedIn: false, loggedIn: false,
setLoggedIn: null, setLoggedIn: null,
userId: 0,
setUserId: null,
username: '', username: '',
setUsername: null, setUsername: null,
displayName: '', displayName: '',
setDisplayName: null, setDisplayName: null,
mode: ModeType.Server,
logout: null logout: null
}) })
@@ -35,8 +46,10 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const { children } = props const { children } = props
const [checkingSession, setCheckingSession] = useState(false) const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false) const [loggedIn, setLoggedIn] = useState(false)
const [userId, setUserId] = useState(0)
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('') const [displayName, setDisplayName] = useState('')
const [mode, setMode] = useState(ModeType.Server)
useEffect(() => { useEffect(() => {
setCheckingSession(true) setCheckingSession(true)
@@ -46,14 +59,23 @@ const AppContextProvider = (props: { children: ReactNode }) => {
.then((res) => res.data) .then((res) => res.data)
.then((data: any) => { .then((data: any) => {
setCheckingSession(false) setCheckingSession(false)
setLoggedIn(true) setUserId(data.id)
setUsername(data.username) setUsername(data.username)
setDisplayName(data.displayName) setDisplayName(data.displayName)
setLoggedIn(true)
}) })
.catch(() => { .catch(() => {
setLoggedIn(false) setLoggedIn(false)
axios.get('/') // get CSRF TOKEN axios.get('/') // get CSRF TOKEN
}) })
axios
.get('/SASjsApi/info')
.then((res) => res.data)
.then((data: any) => {
setMode(data.mode)
})
.catch(() => {})
}, []) }, [])
const logout = useCallback(() => { const logout = useCallback(() => {
@@ -70,10 +92,13 @@ const AppContextProvider = (props: { children: ReactNode }) => {
checkingSession, checkingSession,
loggedIn, loggedIn,
setLoggedIn, setLoggedIn,
userId,
setUserId,
username, username,
setUsername, setUsername,
displayName, displayName,
setDisplayName, setDisplayName,
mode,
logout logout
}} }}
> >