diff --git a/packages/server/package.json b/packages/server/package.json
index 2886e6e8..0a551112 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -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",
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 9dedaf5e..0e981a33 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -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')}/` })
// ----------------------------------------
diff --git a/packages/ui/src/api/client.js b/packages/ui/src/api/client.js
index 1211d67e..cafdf0b3 100644
--- a/packages/ui/src/api/client.js
+++ b/packages/ui/src/api/client.js
@@ -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
diff --git a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.css b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.css
new file mode 100644
index 00000000..f6be27ab
--- /dev/null
+++ b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.css
@@ -0,0 +1,6 @@
+.ps__rail-x {
+ display: none !important;
+}
+.ps__thumb-x {
+ display: none !important;
+}
diff --git a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js
new file mode 100644
index 00000000..c0fa8807
--- /dev/null
+++ b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js
@@ -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 (
+ <>
+
+
+
+
+
+
+ {({ TransitionProps }) => (
+
+
+
+
+
+
+ {username}
+
+
+
+
+
+
+
+
+
+
+ Logout} />
+
+
+
+
+
+
+
+
+ )}
+
+ >
+ )
+}
+
+ProfileSection.propTypes = {
+ username: PropTypes.string,
+ handleLogout: PropTypes.func
+}
+
+export default ProfileSection
diff --git a/packages/ui/src/layout/MainLayout/Header/index.js b/packages/ui/src/layout/MainLayout/Header/index.js
index 30b26702..033eb3a6 100644
--- a/packages/ui/src/layout/MainLayout/Header/index.js
+++ b/packages/ui/src/layout/MainLayout/Header/index.js
@@ -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 }) => {
+ {localStorage.getItem('username') && localStorage.getItem('password') && (
+ <>
+
+
+ >
+ )}
>
)
}
diff --git a/packages/ui/src/ui-component/dialog/LoginDialog.js b/packages/ui/src/ui-component/dialog/LoginDialog.js
new file mode 100644
index 00000000..926a6467
--- /dev/null
+++ b/packages/ui/src/ui-component/dialog/LoginDialog.js
@@ -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 ? (
+
+ ) : null
+
+ return createPortal(component, portalElement)
+}
+
+LoginDialog.propTypes = {
+ show: PropTypes.bool,
+ dialogProps: PropTypes.object,
+ onConfirm: PropTypes.func
+}
+
+export default LoginDialog
diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js
index 0886b22f..1861bf65 100644
--- a/packages/ui/src/ui-component/input/Input.js
+++ b/packages/ui/src/ui-component/input/Input.js
@@ -43,15 +43,17 @@ export const Input = ({ inputParam, value, onChange, disabled = false, showDialo
}}
/>
- {
- setMyValue(newValue)
- onDialogConfirm(newValue, inputParamName)
- }}
- >
+ {showDialog && (
+ {
+ setMyValue(newValue)
+ onDialogConfirm(newValue, inputParamName)
+ }}
+ >
+ )}
>
)
}
diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js
index b5cbd4ae..6712623e 100644
--- a/packages/ui/src/views/chatflows/index.js
+++ b/packages/ui/src/views/chatflows/index.js
@@ -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 = () => {
No Chatflows Yet
)}
+
)
}