Skip to content

Feature: Allowed domains for chatflows #1765

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1319,7 +1319,32 @@ 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[0] !== ''
Copy link
Contributor

@HenryHengZJ HenryHengZJ Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if parsedConfig doesn't have the property allowedOrigins, will it fails?
shouldn't we do something like parsedConfig.allowedOrigins?.length && parsedConfig.allowedOrigins[0] !== '' ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, it does error:
image

if (parsedConfig.allowedOrigins && parsedConfig.allowedOrigins.length > 0 && isValidAllowedOrigins) {
const originHeader = req.headers.origin as string
const origin = new URL(originHeader).host
isDomainAllowed =
parsedConfig.allowedOrigins.filter((domain: string) => {
const allowedOrigin = new URL(domain).host
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if domain is invalid, you will get error Invalid URL and the apps shut down due to unhandled rejection

return origin === allowedOrigin
}).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`)
}
}
)

Expand Down
20 changes: 19 additions & 1 deletion packages/ui/src/menu-items/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ import {
IconSearch,
IconMessage,
IconPictureInPictureOff,
IconLink,
IconMicrophone
} from '@tabler/icons'

// constant
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff, IconMicrophone }
const icons = {
IconTrash,
IconFileUpload,
IconFileExport,
IconCopy,
IconSearch,
IconMessage,
IconPictureInPictureOff,
IconLink,
IconMicrophone
}

// ==============================|| SETTINGS MENU ITEMS ||============================== //

Expand All @@ -34,6 +45,13 @@ const settings = {
url: '',
icon: icons.IconMessage
},
{
id: 'allowedDomains',
title: 'Allowed Domains',
type: 'item',
url: '',
icon: icons.IconLink
},
{
id: 'enableSpeechToText',
title: 'Speech to Text',
Expand Down
214 changes: 214 additions & 0 deletions packages/ui/src/ui-component/dialog/AllowedDomainsDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}

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 ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title || 'Allowed Origins'}
</DialogTitle>
<DialogContent>
<div
style={{
display: 'flex',
flexDirection: 'column'
}}
>
<span>Your chatbot will only work when used from the following domains.</span>
</div>
<Box sx={{ '& > :not(style)': { m: 1 }, pt: 2 }}>
<List>
{inputFields.map((origin, index) => {
return (
<div key={index} style={{ display: 'flex', width: '100%' }}>
<Box sx={{ width: '100%', mb: 1 }}>
<OutlinedInput
sx={{ width: '100%' }}
key={index}
type='text'
onChange={(e) => handleChange(index, e)}
size='small'
value={origin}
name='origin'
endAdornment={
<InputAdornment position='end' sx={{ padding: '2px' }}>
{inputFields.length > 1 && (
<IconButton
sx={{ height: 30, width: 30 }}
size='small'
color='error'
disabled={inputFields.length === 1}
onClick={() => removeInputFields(index)}
edge='end'
>
<IconTrash />
</IconButton>
)}
</InputAdornment>
}
/>
</Box>
<Box sx={{ width: '5%', mb: 1 }}>
{index === inputFields.length - 1 && (
<IconButton color='primary' onClick={addInputField}>
<IconPlus />
</IconButton>
)}
</Box>
</div>
)
})}
</List>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>Cancel</Button>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
</DialogActions>
</Dialog>
) : null

return createPortal(component, portalElement)
}

AllowedDomainsDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}

export default AllowedDomainsDialog
15 changes: 15 additions & 0 deletions packages/ui/src/views/canvas/CanvasHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import APICodeDialog from 'views/chatflows/APICodeDialog'
import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog'
import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog'
import StarterPromptsDialog from 'ui-component/dialog/StarterPromptsDialog'
import AllowedDomainsDialog from 'ui-component/dialog/AllowedDomainsDialog'

// API
import chatflowsApi from 'api/chatflows'
Expand Down Expand Up @@ -53,6 +54,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
const [allowedDomainsDialogOpen, setAllowedDomainsDialogOpen] = useState(false)
const [allowedDomainsDialogProps, setAllowedDomainsDialogProps] = useState({})

const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const canvas = useSelector((state) => state.canvas)
Expand All @@ -68,6 +71,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
chatflow: chatflow
})
setConversationStartersDialogOpen(true)
} else if (setting === 'allowedDomains') {
setAllowedDomainsDialogProps({
title: 'Allowed Domains - ' + chatflow.name,
chatflow: chatflow
})
setAllowedDomainsDialogOpen(true)
} else if (setting === 'analyseChatflow') {
setAnalyseDialogProps({
title: 'Analyse Chatflow',
Expand Down Expand Up @@ -405,6 +414,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
onConfirm={() => setConversationStartersDialogOpen(false)}
onCancel={() => setConversationStartersDialogOpen(false)}
/>
<AllowedDomainsDialog
show={allowedDomainsDialogOpen}
dialogProps={allowedDomainsDialogProps}
onConfirm={() => setAllowedDomainsDialogOpen(false)}
onCancel={() => setAllowedDomainsDialogOpen(false)}
/>
<ViewMessagesDialog
show={viewMessagesDialogOpen}
dialogProps={viewMessagesDialogProps}
Expand Down