2
0

Customizable allowed origins

This commit is contained in:
Baptiste Arnaud
2024-01-17 09:04:07 +01:00
parent b2f8cd44b8
commit 8771def9a1
12 changed files with 151 additions and 5 deletions

View File

@ -0,0 +1,50 @@
import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
import { Settings } from '@typebot.io/schemas'
import React from 'react'
import { isDefined } from '@typebot.io/lib'
import { TextInput } from '@/components/inputs'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { PrimitiveList } from '@/components/PrimitiveList'
type Props = {
security: Settings['security']
onUpdate: (security: Settings['security']) => void
}
export const SecurityForm = ({ security, onUpdate }: Props) => {
const updateItems = (items: string[]) => {
if (items.length === 0) onUpdate(undefined)
onUpdate({
allowedOrigins: items.filter(isDefined),
})
}
return (
<Stack spacing={6}>
<FormControl>
<FormLabel display="flex" flexShrink={0} gap="1" mr="0" mb="4">
Allowed origins
<MoreInfoTooltip>
Restrict the execution of your typebot to specific website origins.
By default your bot can be executed on any website.
</MoreInfoTooltip>
</FormLabel>
<PrimitiveList
initialItems={security?.allowedOrigins}
onItemsChange={updateItems}
addLabel="Add URL"
>
{({ item, onItemChange }) => (
<TextInput
width="full"
defaultValue={item}
onChange={onItemChange}
placeholder={env.NEXT_PUBLIC_VIEWER_URL[0]}
/>
)}
</PrimitiveList>
</FormControl>
</Stack>
)
}

View File

@ -8,7 +8,12 @@ import {
HStack,
Stack,
} from '@chakra-ui/react'
import { ChatIcon, CodeIcon, MoreVerticalIcon } from '@/components/icons'
import {
ChatIcon,
CodeIcon,
LockedIcon,
MoreVerticalIcon,
} from '@/components/icons'
import { Settings } from '@typebot.io/schemas'
import React from 'react'
import { GeneralSettingsForm } from './GeneralSettingsForm'
@ -16,6 +21,7 @@ import { MetadataForm } from './MetadataForm'
import { TypingEmulationForm } from './TypingEmulationForm'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { headerHeight } from '@/features/editor/constants'
import { SecurityForm } from './SecurityForm'
export const SettingsSideMenu = () => {
const { typebot, updateTypebot } = useTypebot()
@ -28,6 +34,12 @@ export const SettingsSideMenu = () => {
updates: { settings: { ...typebot.settings, typingEmulation } },
})
const updateSecurity = (security: Settings['security']) =>
typebot &&
updateTypebot({
updates: { settings: { ...typebot.settings, security } },
})
const handleGeneralSettingsChange = (general: Settings['general']) =>
typebot &&
updateTypebot({ updates: { settings: { ...typebot.settings, general } } })
@ -85,6 +97,23 @@ export const SettingsSideMenu = () => {
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<LockedIcon />
<Heading fontSize="lg">Security</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} px="6">
{typebot && (
<SecurityForm
security={typebot.settings.security}
onUpdate={updateSecurity}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>

View File

@ -27457,6 +27457,17 @@
"type": "boolean"
}
}
},
"security": {
"type": "object",
"properties": {
"allowedOrigins": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"title": "Settings"

View File

@ -7679,6 +7679,17 @@
"type": "boolean"
}
}
},
"security": {
"type": "object",
"properties": {
"allowedOrigins": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"title": "Settings"

View File

@ -36,6 +36,17 @@ return new Promise((res) => setTimeout(res, 3000))
You can tweak `3000` (3s) to your liking.
## Security
By default, your typebot can be executed from any origin but you can restrict the execution of your typebot to specific origins. This is useful if you want to embed your typebot in your website and prevent it from being executed on other websites by malicious actors.
For example, if you want to allow your typebot to be executed only on `https://my-company.com`, you can add `https://my-company.com` to the list of allowed origins.
<Warning>
If you add a URL to the list but omit https://typebot.co, then your typebot
shareable URL will not work anymore.
</Warning>
## Metadata
In the Metadata section, you can customize how the preview card will look if you share your bot URL on social media for example.

View File

@ -28,7 +28,7 @@ export const continueChat = publicProcedure
})
)
.output(continueChatResponseSchema)
.mutation(async ({ input: { sessionId, message } }) => {
.mutation(async ({ input: { sessionId, message }, ctx: { res, origin } }) => {
const session = await getSession(sessionId)
if (!session) {
@ -49,6 +49,15 @@ export const continueChat = publicProcedure
message: 'Session expired. You need to start a new session.',
})
if (
session?.state.allowedOrigins &&
session.state.allowedOrigins.length > 0
) {
if (origin && session.state.allowedOrigins.includes(origin))
res.setHeader('Access-Control-Allow-Origin', origin)
else res.removeHeader('Access-Control-Allow-Origin')
}
const {
messages,
input,

View File

@ -28,6 +28,7 @@ export const startChat = publicProcedure
prefilledVariables,
resultId: startResultId,
},
ctx: { origin, res },
}) => {
const {
typebot,
@ -52,6 +53,15 @@ export const startChat = publicProcedure
message,
})
if (
newSessionState.allowedOrigins &&
newSessionState.allowedOrigins.length > 0
) {
if (origin && newSessionState.allowedOrigins.includes(origin))
res.setHeader('Access-Control-Allow-Origin', origin)
else res.removeHeader('Access-Control-Allow-Origin')
}
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,

View File

@ -11,6 +11,8 @@ export async function createContext(opts: trpcNext.CreateNextContextOptions) {
return {
user,
origin: opts.req.headers.origin,
res: opts.res,
}
}

View File

@ -131,6 +131,10 @@ export const startSession = async ({
dynamicTheme: parseDynamicThemeInState(typebot.theme),
isStreamEnabled: startParams.isStreamEnabled,
typingEmulation: typebot.settings.typingEmulation,
allowedOrigins:
startParams.type === 'preview'
? undefined
: typebot.settings.security?.allowedOrigins,
...initialSessionState,
}

View File

@ -88,6 +88,7 @@ const sessionStateSchemaV3 = sessionStateSchemaV2
.extend({
version: z.literal('3'),
currentBlockId: z.string().optional(),
allowedOrigins: z.array(z.string()).optional(),
})
export type SessionState = z.infer<typeof sessionStateSchemaV3>

View File

@ -42,6 +42,11 @@ export const settingsSchema = z
isEnabled: z.boolean().optional(),
})
.optional(),
security: z
.object({
allowedOrigins: z.array(z.string()).optional(),
})
.optional(),
})
.openapi({
title: 'Settings',

View File

@ -20,9 +20,11 @@ export const deepParseVariables =
},
parseVariablesOptions: ParseVariablesOptions = defaultParseVariablesOptions
) =>
<T extends Record<string, unknown>>(object: T): T =>
Object.keys(object).reduce<T>((newObj, key) => {
const currentValue = object[key]
<T>(object: T): T => {
if (!object) return object as T
if (typeof object !== 'object') return object as T
return Object.keys(object).reduce<T>((newObj, key) => {
const currentValue = (object as Record<string, unknown>)[key]
if (typeof currentValue === 'string') {
const parsedVariable = parseVariables(
@ -63,3 +65,4 @@ export const deepParseVariables =
return { ...newObj, [key]: currentValue }
}, {} as T)
}