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 ? ( + { + if (e.key === 'Enter') { + onConfirm(usernameVal, passwordVal) + } + }} + open={show} + fullWidth + maxWidth='xs' + aria-labelledby='alert-dialog-title' + aria-describedby='alert-dialog-description' + > + + {dialogProps.title} + + + Username + setUsernameVal(newValue)} + value={usernameVal} + showDialog={false} + /> +
+ Password + setPasswordVal(newValue)} value={passwordVal} /> +
+ + onConfirm(usernameVal, passwordVal)}> + {dialogProps.confirmButtonName} + + +
+ ) : 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
)} + ) }