mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-22 09:01:09 +03:00
add authorization
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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')}/` })
|
||||
|
||||
// ----------------------------------------
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user