Skip to content

Commit

Permalink
✨ Customizable allowed origins
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Jan 17, 2024
1 parent b2f8cd4 commit 8771def
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 5 deletions.
50 changes: 50 additions & 0 deletions apps/builder/src/features/settings/components/SecurityForm.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ 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'
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()
Expand All @@ -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 } } })
Expand Down Expand Up @@ -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}>
Expand Down
11 changes: 11 additions & 0 deletions apps/docs/openapi/builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -27457,6 +27457,17 @@
"type": "boolean"
}
}
},
"security": {
"type": "object",
"properties": {
"allowedOrigins": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"title": "Settings"
Expand Down
11 changes: 11 additions & 0 deletions apps/docs/openapi/viewer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7679,6 +7679,17 @@
"type": "boolean"
}
}
},
"security": {
"type": "object",
"properties": {
"allowedOrigins": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"title": "Settings"
Expand Down
11 changes: 11 additions & 0 deletions apps/docs/settings/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion apps/viewer/src/features/chat/api/continueChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions apps/viewer/src/features/chat/api/startChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const startChat = publicProcedure
prefilledVariables,
resultId: startResultId,
},
ctx: { origin, res },
}) => {
const {
typebot,
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions apps/viewer/src/helpers/server/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export async function createContext(opts: trpcNext.CreateNextContextOptions) {

return {
user,
origin: opts.req.headers.origin,
res: opts.res,
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/bot-engine/startSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
1 change: 1 addition & 0 deletions packages/schemas/features/chat/sessionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions packages/schemas/features/typebot/settings/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 6 additions & 3 deletions packages/variables/deepParseVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -63,3 +65,4 @@ export const deepParseVariables =

return { ...newObj, [key]: currentValue }
}, {} as T)
}

2 comments on commit 8771def

@vercel
Copy link

@vercel vercel bot commented on 8771def Jan 17, 2024

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-typebot-io.vercel.app
builder-v2-git-main-typebot-io.vercel.app
app.typebot.io

@vercel
Copy link

@vercel vercel bot commented on 8771def Jan 17, 2024

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

landing-page-v2 – ./apps/landing-page

landing-page-v2-typebot-io.vercel.app
landing-page-v2-git-main-typebot-io.vercel.app
home.typebot.io

Please sign in to comment.