diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 56a3451a..3d5986b0 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1433,7 +1433,36 @@ export class App { upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), async (req: Request, res: Response) => { - await this.buildChatflow(req, res, socketIO) + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + let isDomainAllowed = true + logger.info(`[server]: Request originated from ${req.headers.origin}`) + if (chatflow.chatbotConfig) { + const parsedConfig = JSON.parse(chatflow.chatbotConfig) + // check whether the first one is not empty. if it is empty that means the user set a value and then removed it. + const isValidAllowedOrigins = parsedConfig.allowedOrigins?.length && parsedConfig.allowedOrigins[0] !== '' + if (isValidAllowedOrigins) { + const originHeader = req.headers.origin as string + const origin = new URL(originHeader).host + isDomainAllowed = + parsedConfig.allowedOrigins.filter((domain: string) => { + try { + const allowedOrigin = new URL(domain).host + return origin === allowedOrigin + } catch (e) { + return false + } + }).length > 0 + } + } + + if (isDomainAllowed) { + await this.buildChatflow(req, res, socketIO) + } else { + return res.status(401).send(`This site is not allowed to access this chatbot`) + } } ) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index f5f2d653..f1fd27ab 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -493,13 +493,14 @@ export const clearSessionMemory = async ( * @returns {string} */ export const getVariableValue = ( - paramValue: string, + paramValue: string | object, reactFlowNodes: IReactFlowNode[], question: string, chatHistory: IMessage[], isAcceptVariable = false ) => { - let returnVal = paramValue + const isObject = typeof paramValue === 'object' + let returnVal = isObject ? JSON.stringify(paramValue) : paramValue const variableStack = [] const variableDict = {} as IVariableDict let startIdx = 0 @@ -596,7 +597,7 @@ export const getVariableValue = ( }) return returnVal } - return returnVal + return isObject ? JSON.parse(returnVal) : returnVal } /** diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 40104194..8c7d08b8 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -7,6 +7,7 @@ import { IconSearch, IconMessage, IconPictureInPictureOff, + IconLink, IconMicrophone, IconThumbUp } from '@tabler/icons' @@ -20,6 +21,7 @@ const icons = { IconSearch, IconMessage, IconPictureInPictureOff, + IconLink, IconMicrophone, IconThumbUp } @@ -52,6 +54,13 @@ const settings = { url: '', icon: icons.IconThumbUp }, + { + id: 'allowedDomains', + title: 'Allowed Domains', + type: 'item', + url: '', + icon: icons.IconLink + }, { id: 'enableSpeechToText', title: 'Speech to Text', diff --git a/packages/ui/src/ui-component/dialog/AllowedDomainsDialog.js b/packages/ui/src/ui-component/dialog/AllowedDomainsDialog.js new file mode 100644 index 00000000..8fd57fb6 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/AllowedDomainsDialog.js @@ -0,0 +1,215 @@ +import { createPortal } from 'react-dom' +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions' + +// material-ui +import { + Button, + IconButton, + Dialog, + DialogContent, + OutlinedInput, + DialogTitle, + DialogActions, + Box, + List, + InputAdornment +} from '@mui/material' +import { IconX, IconTrash, IconPlus } from '@tabler/icons' + +// Project import +import { StyledButton } from 'ui-component/button/StyledButton' + +// store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import useNotifier from 'utils/useNotifier' + +// API +import chatflowsApi from 'api/chatflows' + +const AllowedDomainsDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [inputFields, setInputFields] = useState(['']) + + const [chatbotConfig, setChatbotConfig] = useState({}) + + const addInputField = () => { + setInputFields([...inputFields, '']) + } + const removeInputFields = (index) => { + const rows = [...inputFields] + rows.splice(index, 1) + setInputFields(rows) + } + + const handleChange = (index, evnt) => { + const { value } = evnt.target + const list = [...inputFields] + list[index] = value + setInputFields(list) + } + + const onSave = async () => { + try { + let value = { + allowedOrigins: [...inputFields] + } + chatbotConfig.allowedOrigins = value.allowedOrigins + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(chatbotConfig) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Allowed Origins Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + onConfirm() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Allowed Origins: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { + try { + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) + if (chatbotConfig.allowedOrigins) { + let inputFields = [...chatbotConfig.allowedOrigins] + setInputFields(inputFields) + } else { + setInputFields(['']) + } + } catch (e) { + setInputFields(['']) + } + } + + return () => {} + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + {dialogProps.title || 'Allowed Origins'} + + +
+ Your chatbot will only work when used from the following domains. +
+ :not(style)': { m: 1 }, pt: 2 }}> + + {inputFields.map((origin, index) => { + return ( +
+ + handleChange(index, e)} + size='small' + value={origin} + name='origin' + placeholder='https://example.com' + endAdornment={ + + {inputFields.length > 1 && ( + removeInputFields(index)} + edge='end' + > + + + )} + + } + /> + + + {index === inputFields.length - 1 && ( + + + + )} + +
+ ) + })} +
+
+
+ + + + Save + + +
+ ) : null + + return createPortal(component, portalElement) +} + +AllowedDomainsDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default AllowedDomainsDialog diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index 895f0192..86a84b67 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -18,6 +18,7 @@ import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog' import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog' import StarterPromptsDialog from 'ui-component/dialog/StarterPromptsDialog' import ChatFeedbackDialog from 'ui-component/dialog/ChatFeedbackDialog' +import AllowedDomainsDialog from 'ui-component/dialog/AllowedDomainsDialog' // API import chatflowsApi from 'api/chatflows' @@ -56,6 +57,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({}) const [chatFeedbackDialogOpen, setChatFeedbackDialogOpen] = useState(false) const [chatFeedbackDialogProps, setChatFeedbackDialogProps] = useState({}) + const [allowedDomainsDialogOpen, setAllowedDomainsDialogOpen] = useState(false) + const [allowedDomainsDialogProps, setAllowedDomainsDialogProps] = useState({}) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) const canvas = useSelector((state) => state.canvas) @@ -77,6 +80,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl chatflow: chatflow }) setChatFeedbackDialogOpen(true) + } else if (setting === 'allowedDomains') { + setAllowedDomainsDialogProps({ + title: 'Allowed Domains - ' + chatflow.name, + chatflow: chatflow + }) + setAllowedDomainsDialogOpen(true) } else if (setting === 'analyseChatflow') { setAnalyseDialogProps({ title: 'Analyse Chatflow', @@ -420,6 +429,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl onConfirm={() => setChatFeedbackDialogOpen(false)} onCancel={() => setChatFeedbackDialogOpen(false)} /> + setAllowedDomainsDialogOpen(false)} + onCancel={() => setAllowedDomainsDialogOpen(false)} + /> { const onDialogClicked = () => { const dialogProps = { data, - inputParams: data.inputParams.filter((param) => param.additionalParams), + inputParams: data.inputParams.filter((inputParam) => !inputParam.hidden).filter((param) => param.additionalParams), confirmButtonName: 'Save', cancelButtonName: 'Cancel' }