mirror of
https://github.com/sasjs/server.git
synced 2025-12-12 03:54:34 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afff27fd21 | ||
|
|
a8ba378fd1 | ||
|
|
73c81a45dc | ||
|
|
12d424acce | ||
|
|
414fb19de3 | ||
|
|
cfddf1fb0c | ||
|
|
1f483b1afc | ||
|
|
0470239ef1 | ||
|
|
2c259fe1de |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,3 +1,24 @@
|
|||||||
|
## [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)
|
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"tmp/**/*"
|
"sasjs_root/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } =
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +192,14 @@ const getUser = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDesktopAutoExec = async () => {
|
||||||
|
return {
|
||||||
|
...desktopUser,
|
||||||
|
id: desktopUser.userId,
|
||||||
|
autoExec: await getUserAutoExec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateUser = async (
|
const updateUser = async (
|
||||||
id: number,
|
id: number,
|
||||||
data: Partial<UserPayload>
|
data: Partial<UserPayload>
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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!')
|
||||||
|
|||||||
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 './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'
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
import { AppContext } from '../../context/appContext'
|
import { AppContext, ModeType } from '../../context/appContext'
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@@ -89,6 +89,7 @@ const Profile = () => {
|
|||||||
required
|
required
|
||||||
value={user.displayName}
|
value={user.displayName}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
disabled={appContext.mode === ModeType.Desktop}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ const Profile = () => {
|
|||||||
required
|
required
|
||||||
value={user.username}
|
value={user.username}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
disabled={appContext.mode === ModeType.Desktop}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ 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
|
||||||
@@ -19,6 +24,7 @@ interface AppContextProps {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +38,7 @@ export const AppContext = createContext<AppContextProps>({
|
|||||||
setUsername: null,
|
setUsername: null,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
setDisplayName: null,
|
setDisplayName: null,
|
||||||
|
mode: ModeType.Server,
|
||||||
logout: null
|
logout: null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -42,6 +49,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
const [userId, setUserId] = useState(0)
|
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)
|
||||||
@@ -60,6 +68,14 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
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(() => {
|
||||||
@@ -82,6 +98,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
setUsername,
|
setUsername,
|
||||||
displayName,
|
displayName,
|
||||||
setDisplayName,
|
setDisplayName,
|
||||||
|
mode,
|
||||||
logout
|
logout
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user