✨ Customizable allowed origins
This commit is contained in:
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -8,7 +8,12 @@ import {
|
|||||||
HStack,
|
HStack,
|
||||||
Stack,
|
Stack,
|
||||||
} from '@chakra-ui/react'
|
} 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 { Settings } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { GeneralSettingsForm } from './GeneralSettingsForm'
|
import { GeneralSettingsForm } from './GeneralSettingsForm'
|
||||||
@ -16,6 +21,7 @@ import { MetadataForm } from './MetadataForm'
|
|||||||
import { TypingEmulationForm } from './TypingEmulationForm'
|
import { TypingEmulationForm } from './TypingEmulationForm'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { headerHeight } from '@/features/editor/constants'
|
import { headerHeight } from '@/features/editor/constants'
|
||||||
|
import { SecurityForm } from './SecurityForm'
|
||||||
|
|
||||||
export const SettingsSideMenu = () => {
|
export const SettingsSideMenu = () => {
|
||||||
const { typebot, updateTypebot } = useTypebot()
|
const { typebot, updateTypebot } = useTypebot()
|
||||||
@ -28,6 +34,12 @@ export const SettingsSideMenu = () => {
|
|||||||
updates: { settings: { ...typebot.settings, typingEmulation } },
|
updates: { settings: { ...typebot.settings, typingEmulation } },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updateSecurity = (security: Settings['security']) =>
|
||||||
|
typebot &&
|
||||||
|
updateTypebot({
|
||||||
|
updates: { settings: { ...typebot.settings, security } },
|
||||||
|
})
|
||||||
|
|
||||||
const handleGeneralSettingsChange = (general: Settings['general']) =>
|
const handleGeneralSettingsChange = (general: Settings['general']) =>
|
||||||
typebot &&
|
typebot &&
|
||||||
updateTypebot({ updates: { settings: { ...typebot.settings, general } } })
|
updateTypebot({ updates: { settings: { ...typebot.settings, general } } })
|
||||||
@ -85,6 +97,23 @@ export const SettingsSideMenu = () => {
|
|||||||
)}
|
)}
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</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>
|
<AccordionItem>
|
||||||
<AccordionButton py={6}>
|
<AccordionButton py={6}>
|
||||||
<HStack flex="1" pl={2}>
|
<HStack flex="1" pl={2}>
|
||||||
|
@ -27457,6 +27457,17 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allowedOrigins": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Settings"
|
"title": "Settings"
|
||||||
|
@ -7679,6 +7679,17 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allowedOrigins": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Settings"
|
"title": "Settings"
|
||||||
|
@ -36,6 +36,17 @@ return new Promise((res) => setTimeout(res, 3000))
|
|||||||
|
|
||||||
You can tweak `3000` (3s) to your liking.
|
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
|
## 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.
|
In the Metadata section, you can customize how the preview card will look if you share your bot URL on social media for example.
|
||||||
|
@ -28,7 +28,7 @@ export const continueChat = publicProcedure
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.output(continueChatResponseSchema)
|
.output(continueChatResponseSchema)
|
||||||
.mutation(async ({ input: { sessionId, message } }) => {
|
.mutation(async ({ input: { sessionId, message }, ctx: { res, origin } }) => {
|
||||||
const session = await getSession(sessionId)
|
const session = await getSession(sessionId)
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@ -49,6 +49,15 @@ export const continueChat = publicProcedure
|
|||||||
message: 'Session expired. You need to start a new session.',
|
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 {
|
const {
|
||||||
messages,
|
messages,
|
||||||
input,
|
input,
|
||||||
|
@ -28,6 +28,7 @@ export const startChat = publicProcedure
|
|||||||
prefilledVariables,
|
prefilledVariables,
|
||||||
resultId: startResultId,
|
resultId: startResultId,
|
||||||
},
|
},
|
||||||
|
ctx: { origin, res },
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
typebot,
|
typebot,
|
||||||
@ -52,6 +53,15 @@ export const startChat = publicProcedure
|
|||||||
message,
|
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
|
const session = isOnlyRegistering
|
||||||
? await restartSession({
|
? await restartSession({
|
||||||
state: newSessionState,
|
state: newSessionState,
|
||||||
|
@ -11,6 +11,8 @@ export async function createContext(opts: trpcNext.CreateNextContextOptions) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
origin: opts.req.headers.origin,
|
||||||
|
res: opts.res,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +131,10 @@ export const startSession = async ({
|
|||||||
dynamicTheme: parseDynamicThemeInState(typebot.theme),
|
dynamicTheme: parseDynamicThemeInState(typebot.theme),
|
||||||
isStreamEnabled: startParams.isStreamEnabled,
|
isStreamEnabled: startParams.isStreamEnabled,
|
||||||
typingEmulation: typebot.settings.typingEmulation,
|
typingEmulation: typebot.settings.typingEmulation,
|
||||||
|
allowedOrigins:
|
||||||
|
startParams.type === 'preview'
|
||||||
|
? undefined
|
||||||
|
: typebot.settings.security?.allowedOrigins,
|
||||||
...initialSessionState,
|
...initialSessionState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +88,7 @@ const sessionStateSchemaV3 = sessionStateSchemaV2
|
|||||||
.extend({
|
.extend({
|
||||||
version: z.literal('3'),
|
version: z.literal('3'),
|
||||||
currentBlockId: z.string().optional(),
|
currentBlockId: z.string().optional(),
|
||||||
|
allowedOrigins: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type SessionState = z.infer<typeof sessionStateSchemaV3>
|
export type SessionState = z.infer<typeof sessionStateSchemaV3>
|
||||||
|
@ -42,6 +42,11 @@ export const settingsSchema = z
|
|||||||
isEnabled: z.boolean().optional(),
|
isEnabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
security: z
|
||||||
|
.object({
|
||||||
|
allowedOrigins: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
|
@ -20,9 +20,11 @@ export const deepParseVariables =
|
|||||||
},
|
},
|
||||||
parseVariablesOptions: ParseVariablesOptions = defaultParseVariablesOptions
|
parseVariablesOptions: ParseVariablesOptions = defaultParseVariablesOptions
|
||||||
) =>
|
) =>
|
||||||
<T extends Record<string, unknown>>(object: T): T =>
|
<T>(object: T): T => {
|
||||||
Object.keys(object).reduce<T>((newObj, key) => {
|
if (!object) return object as T
|
||||||
const currentValue = object[key]
|
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') {
|
if (typeof currentValue === 'string') {
|
||||||
const parsedVariable = parseVariables(
|
const parsedVariable = parseVariables(
|
||||||
@ -63,3 +65,4 @@ export const deepParseVariables =
|
|||||||
|
|
||||||
return { ...newObj, [key]: currentValue }
|
return { ...newObj, [key]: currentValue }
|
||||||
}, {} as T)
|
}, {} as T)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user