add authorization

This commit is contained in:
Henry
2023-05-13 23:10:59 +01:00
parent 8270707668
commit 05bd7bc793
9 changed files with 317 additions and 9 deletions
+1
View File
@@ -51,6 +51,7 @@
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-basic-auth": "^1.2.1",
"flowise-components": "*",
"flowise-ui": "*",
"moment-timezone": "^0.5.34",
+13
View File
@@ -4,6 +4,7 @@ import path from 'path'
import cors from 'cors'
import http from 'http'
import * as fs from 'fs'
import basicAuth from 'express-basic-auth'
import { IChatFlow, IncomingInput, IReactFlowNode, IReactFlowObject, INodeData } from './Interface'
import {
@@ -69,6 +70,18 @@ export class App {
// Allow access from *
this.app.use(cors())
if (process.env.USERNAME && process.env.PASSWORD) {
const username = process.env.USERNAME.toLocaleLowerCase()
const password = process.env.PASSWORD.toLocaleLowerCase()
const basicAuthMiddleware = basicAuth({
users: { [username]: password }
})
const whitelistURLs = ['node-icon', 'static', 'favicon']
this.app.use((req, res, next) =>
whitelistURLs.some((url) => req.url.includes(url)) || req.url === '/' ? next() : basicAuthMiddleware(req, res, next)
)
}
const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` })
// ----------------------------------------
+14
View File
@@ -8,4 +8,18 @@ const apiClient = axios.create({
}
})
apiClient.interceptors.request.use(function (config) {
const username = localStorage.getItem('username')
const password = localStorage.getItem('password')
if (username && password) {
config.auth = {
username: username.toLocaleLowerCase(),
password: password.toLocaleLowerCase()
}
}
return config
})
export default apiClient
@@ -0,0 +1,6 @@
.ps__rail-x {
display: none !important;
}
.ps__thumb-x {
display: none !important;
}
@@ -0,0 +1,163 @@
import { useState, useRef, useEffect } from 'react'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import {
Box,
ButtonBase,
Avatar,
ClickAwayListener,
Divider,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
Popper,
Typography
} from '@mui/material'
// third-party
import PerfectScrollbar from 'react-perfect-scrollbar'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import Transitions from 'ui-component/extended/Transitions'
// assets
import { IconLogout, IconSettings } from '@tabler/icons'
import './index.css'
// ==============================|| PROFILE MENU ||============================== //
const ProfileSection = ({ username, handleLogout }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [open, setOpen] = useState(false)
const anchorRef = useRef(null)
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return
}
setOpen(false)
}
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen)
}
const prevOpen = useRef(open)
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus()
}
prevOpen.current = open
}, [open])
return (
<>
<ButtonBase ref={anchorRef} sx={{ borderRadius: '12px', overflow: 'hidden' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
onClick={handleToggle}
color='inherit'
>
<IconSettings stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
<Popper
placement='bottom-end'
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [0, 14]
}
}
]
}}
>
{({ TransitionProps }) => (
<Transitions in={open} {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<Box sx={{ p: 2 }}>
<Typography component='span' variant='h4'>
{username}
</Typography>
</Box>
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 250px)', overflowX: 'hidden' }}>
<Box sx={{ p: 2 }}>
<Divider />
<List
component='nav'
sx={{
width: '100%',
maxWidth: 250,
minWidth: 200,
backgroundColor: theme.palette.background.paper,
borderRadius: '10px',
[theme.breakpoints.down('md')]: {
minWidth: '100%'
},
'& .MuiListItemButton-root': {
mt: 0.5
}
}}
>
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={handleLogout}
>
<ListItemIcon>
<IconLogout stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
</ListItemButton>
</List>
</Box>
</PerfectScrollbar>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</>
)
}
ProfileSection.propTypes = {
username: PropTypes.string,
handleLogout: PropTypes.func
}
export default ProfileSection
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
// material-ui
import { useTheme } from '@mui/material/styles'
@@ -9,6 +10,7 @@ import { styled } from '@mui/material/styles'
// project imports
import LogoSection from '../LogoSection'
import ProfileSection from './ProfileSection'
// assets
import { IconMenu2 } from '@tabler/icons'
@@ -67,6 +69,8 @@ const MaterialUISwitch = styled(Switch)(({ theme }) => ({
const Header = ({ handleLeftDrawerToggle }) => {
const theme = useTheme()
const navigate = useNavigate()
const customization = useSelector((state) => state.customization)
const [isDark, setIsDark] = useState(customization.isDarkMode)
@@ -78,6 +82,13 @@ const Header = ({ handleLeftDrawerToggle }) => {
localStorage.setItem('isDarkMode', !isDark)
}
const signOutClicked = () => {
localStorage.removeItem('username')
localStorage.removeItem('password')
navigate('/', { replace: true })
navigate(0)
}
return (
<>
{/* logo & toggler button */}
@@ -116,6 +127,12 @@ const Header = ({ handleLeftDrawerToggle }) => {
</Box>
<Box sx={{ flexGrow: 1 }} />
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
{localStorage.getItem('username') && localStorage.getItem('password') && (
<>
<Box sx={{ ml: 2 }}></Box>
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? 'user'} />
</>
)}
</>
)
}
@@ -0,0 +1,70 @@
import { createPortal } from 'react-dom'
import { useState } from 'react'
import PropTypes from 'prop-types'
import { Dialog, DialogActions, DialogContent, Typography, DialogTitle } from '@mui/material'
import { StyledButton } from 'ui-component/button/StyledButton'
import { Input } from 'ui-component/input/Input'
const LoginDialog = ({ show, dialogProps, onConfirm }) => {
const portalElement = document.getElementById('portal')
const usernameInput = {
label: 'Username',
name: 'username',
type: 'string',
placeholder: 'john doe'
}
const passwordInput = {
label: 'Password',
name: 'password',
type: 'password'
}
const [usernameVal, setUsernameVal] = useState('')
const [passwordVal, setPasswordVal] = useState('')
const component = show ? (
<Dialog
onKeyUp={(e) => {
if (e.key === 'Enter') {
onConfirm(usernameVal, passwordVal)
}
}}
open={show}
fullWidth
maxWidth='xs'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<Typography>Username</Typography>
<Input
inputParam={usernameInput}
onChange={(newValue) => setUsernameVal(newValue)}
value={usernameVal}
showDialog={false}
/>
<div style={{ marginTop: 20 }}></div>
<Typography>Password</Typography>
<Input inputParam={passwordInput} onChange={(newValue) => setPasswordVal(newValue)} value={passwordVal} />
</DialogContent>
<DialogActions>
<StyledButton variant='contained' onClick={() => onConfirm(usernameVal, passwordVal)}>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
LoginDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onConfirm: PropTypes.func
}
export default LoginDialog
+11 -9
View File
@@ -43,15 +43,17 @@ export const Input = ({ inputParam, value, onChange, disabled = false, showDialo
}}
/>
</FormControl>
<EditPromptValuesDialog
show={showDialog}
dialogProps={dialogProps}
onCancel={onDialogCancel}
onConfirm={(newValue, inputParamName) => {
setMyValue(newValue)
onDialogConfirm(newValue, inputParamName)
}}
></EditPromptValuesDialog>
{showDialog && (
<EditPromptValuesDialog
show={showDialog}
dialogProps={dialogProps}
onCancel={onDialogCancel}
onConfirm={(newValue, inputParamName) => {
setMyValue(newValue)
onDialogConfirm(newValue, inputParamName)
}}
></EditPromptValuesDialog>
)}
</>
)
}
+22
View File
@@ -12,6 +12,7 @@ import ItemCard from 'ui-component/cards/ItemCard'
import { gridSpacing } from 'store/constant'
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
import { StyledButton } from 'ui-component/button/StyledButton'
import LoginDialog from 'ui-component/dialog/LoginDialog'
// API
import chatflowsApi from 'api/chatflows'
@@ -34,9 +35,17 @@ const Chatflows = () => {
const [isLoading, setLoading] = useState(true)
const [images, setImages] = useState({})
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
const [loginDialogProps, setLoginDialogProps] = useState({})
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
const onLoginClick = (username, password) => {
localStorage.setItem('username', username)
localStorage.setItem('password', password)
navigate(0)
}
const addNew = () => {
navigate('/canvas')
}
@@ -51,6 +60,18 @@ const Chatflows = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllChatflowsApi.error) {
if (getAllChatflowsApi.error?.response?.status === 401) {
setLoginDialogProps({
title: 'Login',
confirmButtonName: 'Login'
})
setLoginDialogOpen(true)
}
}
}, [getAllChatflowsApi.error])
useEffect(() => {
setLoading(getAllChatflowsApi.loading)
}, [getAllChatflowsApi.loading])
@@ -109,6 +130,7 @@ const Chatflows = () => {
<div>No Chatflows Yet</div>
</Stack>
)}
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
</MainCard>
)
}