mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 21:00:58 +03:00
Merge pull request #103 from FlowiseAI/feature/Authorization
Feature/Add App Authorization
This commit is contained in:
@@ -86,6 +86,15 @@ Flowise has 3 different modules in a single mono repository.
|
|||||||
|
|
||||||
Any code changes will reload the app automatically on [http://localhost:8080](http://localhost:8080)
|
Any code changes will reload the app automatically on [http://localhost:8080](http://localhost:8080)
|
||||||
|
|
||||||
|
## 🔒 Authentication
|
||||||
|
|
||||||
|
To enable app level authentication, add `USERNAME` and `PASSWORD` to the `.env` file in `packages/server`:
|
||||||
|
|
||||||
|
```
|
||||||
|
USERNAME=user
|
||||||
|
PASSWORD=1234
|
||||||
|
```
|
||||||
|
|
||||||
## 📖 Documentation
|
## 📖 Documentation
|
||||||
|
|
||||||
Coming soon
|
Coming soon
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
|
# USERNAME=user
|
||||||
|
# PASSWORD=1234
|
||||||
@@ -20,6 +20,15 @@ Drag & drop UI to build your customized LLM flow using [LangchainJS](https://git
|
|||||||
|
|
||||||
3. Open [http://localhost:3000](http://localhost:3000)
|
3. Open [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
|
## 🔒 Authentication
|
||||||
|
|
||||||
|
To enable app level authentication, add `USERNAME` and `PASSWORD` to the `.env` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
USERNAME=user
|
||||||
|
PASSWORD=1234
|
||||||
|
```
|
||||||
|
|
||||||
## 📖 Documentation
|
## 📖 Documentation
|
||||||
|
|
||||||
Coming Soon
|
Coming Soon
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
|
"express-basic-auth": "^1.2.1",
|
||||||
"flowise-components": "*",
|
"flowise-components": "*",
|
||||||
"flowise-ui": "*",
|
"flowise-ui": "*",
|
||||||
"moment-timezone": "^0.5.34",
|
"moment-timezone": "^0.5.34",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from 'path'
|
|||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
import basicAuth from 'express-basic-auth'
|
||||||
|
|
||||||
import { IChatFlow, IncomingInput, IReactFlowNode, IReactFlowObject, INodeData } from './Interface'
|
import { IChatFlow, IncomingInput, IReactFlowNode, IReactFlowObject, INodeData } from './Interface'
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,18 @@ export class App {
|
|||||||
// Allow access from *
|
// Allow access from *
|
||||||
this.app.use(cors())
|
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 = ['static', 'favicon', '/api/v1/prediction/', '/api/v1/node-icon/']
|
||||||
|
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')}/` })
|
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
|
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 PropTypes from 'prop-types'
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
@@ -9,6 +10,7 @@ import { styled } from '@mui/material/styles'
|
|||||||
|
|
||||||
// project imports
|
// project imports
|
||||||
import LogoSection from '../LogoSection'
|
import LogoSection from '../LogoSection'
|
||||||
|
import ProfileSection from './ProfileSection'
|
||||||
|
|
||||||
// assets
|
// assets
|
||||||
import { IconMenu2 } from '@tabler/icons'
|
import { IconMenu2 } from '@tabler/icons'
|
||||||
@@ -67,6 +69,8 @@ const MaterialUISwitch = styled(Switch)(({ theme }) => ({
|
|||||||
|
|
||||||
const Header = ({ handleLeftDrawerToggle }) => {
|
const Header = ({ handleLeftDrawerToggle }) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const customization = useSelector((state) => state.customization)
|
const customization = useSelector((state) => state.customization)
|
||||||
|
|
||||||
const [isDark, setIsDark] = useState(customization.isDarkMode)
|
const [isDark, setIsDark] = useState(customization.isDarkMode)
|
||||||
@@ -78,6 +82,13 @@ const Header = ({ handleLeftDrawerToggle }) => {
|
|||||||
localStorage.setItem('isDarkMode', !isDark)
|
localStorage.setItem('isDarkMode', !isDark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signOutClicked = () => {
|
||||||
|
localStorage.removeItem('username')
|
||||||
|
localStorage.removeItem('password')
|
||||||
|
navigate('/', { replace: true })
|
||||||
|
navigate(0)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* logo & toggler button */}
|
{/* logo & toggler button */}
|
||||||
@@ -116,6 +127,12 @@ const Header = ({ handleLeftDrawerToggle }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
|
<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>
|
</FormControl>
|
||||||
<EditPromptValuesDialog
|
{showDialog && (
|
||||||
show={showDialog}
|
<EditPromptValuesDialog
|
||||||
dialogProps={dialogProps}
|
show={showDialog}
|
||||||
onCancel={onDialogCancel}
|
dialogProps={dialogProps}
|
||||||
onConfirm={(newValue, inputParamName) => {
|
onCancel={onDialogCancel}
|
||||||
setMyValue(newValue)
|
onConfirm={(newValue, inputParamName) => {
|
||||||
onDialogConfirm(newValue, inputParamName)
|
setMyValue(newValue)
|
||||||
}}
|
onDialogConfirm(newValue, inputParamName)
|
||||||
></EditPromptValuesDialog>
|
}}
|
||||||
|
></EditPromptValuesDialog>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ItemCard from 'ui-component/cards/ItemCard'
|
|||||||
import { gridSpacing } from 'store/constant'
|
import { gridSpacing } from 'store/constant'
|
||||||
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
|
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
|
||||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
|
import LoginDialog from 'ui-component/dialog/LoginDialog'
|
||||||
|
|
||||||
// API
|
// API
|
||||||
import chatflowsApi from 'api/chatflows'
|
import chatflowsApi from 'api/chatflows'
|
||||||
@@ -34,9 +35,17 @@ const Chatflows = () => {
|
|||||||
|
|
||||||
const [isLoading, setLoading] = useState(true)
|
const [isLoading, setLoading] = useState(true)
|
||||||
const [images, setImages] = useState({})
|
const [images, setImages] = useState({})
|
||||||
|
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
|
||||||
|
const [loginDialogProps, setLoginDialogProps] = useState({})
|
||||||
|
|
||||||
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
|
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
|
||||||
|
|
||||||
|
const onLoginClick = (username, password) => {
|
||||||
|
localStorage.setItem('username', username)
|
||||||
|
localStorage.setItem('password', password)
|
||||||
|
navigate(0)
|
||||||
|
}
|
||||||
|
|
||||||
const addNew = () => {
|
const addNew = () => {
|
||||||
navigate('/canvas')
|
navigate('/canvas')
|
||||||
}
|
}
|
||||||
@@ -51,6 +60,18 @@ const Chatflows = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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(() => {
|
useEffect(() => {
|
||||||
setLoading(getAllChatflowsApi.loading)
|
setLoading(getAllChatflowsApi.loading)
|
||||||
}, [getAllChatflowsApi.loading])
|
}, [getAllChatflowsApi.loading])
|
||||||
@@ -109,6 +130,7 @@ const Chatflows = () => {
|
|||||||
<div>No Chatflows Yet</div>
|
<div>No Chatflows Yet</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
|
||||||
</MainCard>
|
</MainCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user