2
0

build: 👷 New compose file and entrypoints

This commit is contained in:
Baptiste Arnaud
2022-05-25 08:13:35 -07:00
parent 4a5a92b973
commit 5d786f59cc
44 changed files with 288 additions and 113 deletions

3
.gitignore vendored
View File

@ -2,7 +2,6 @@ node_modules
.next .next
.env .env
.env.local .env.local
.env.production
workspace.code-workspace workspace.code-workspace
.DS_Store .DS_Store
.turbo .turbo
@ -18,6 +17,8 @@ test-results
build build
firebaseServiceAccount.json firebaseServiceAccount.json
.env.production
# Wordpress # Wordpress
.svn .svn
tags tags

View File

@ -17,6 +17,7 @@ RUN yarn install --frozen-lockfile
FROM base AS builder FROM base AS builder
COPY --from=installer /app/ . COPY --from=installer /app/ .
COPY --from=pruner /app/out/full/ . COPY --from=pruner /app/out/full/ .
COPY ./apps/${SCOPE}/.env.docker ./apps/${SCOPE}/.env.production
RUN apt-get -qy update && apt-get -qy install openssl RUN apt-get -qy update && apt-get -qy install openssl
RUN yarn turbo run build --scope=${SCOPE} --include-dependencies --no-deps RUN yarn turbo run build --scope=${SCOPE} --include-dependencies --no-deps
RUN find . -name node_modules | xargs rm -rf RUN find . -name node_modules | xargs rm -rf
@ -31,7 +32,14 @@ COPY --from=builder /app/apps/${SCOPE}/public ./public
COPY --from=builder /app/apps/${SCOPE}/package.json ./package.json COPY --from=builder /app/apps/${SCOPE}/package.json ./package.json
COPY --from=builder /app/apps/${SCOPE}/.next/standalone ./ COPY --from=builder /app/apps/${SCOPE}/.next/standalone ./
COPY --from=builder /app/apps/${SCOPE}/.next/static ./.next/static COPY --from=builder /app/apps/${SCOPE}/.next/static ./.next/static
COPY --from=builder /app/apps/${SCOPE}/.env.docker ./.env.production
RUN apt-get -qy update && apt-get -qy install openssl RUN apt-get -qy update && apt-get -qy install openssl
COPY entrypoint.sh ./
COPY ${SCOPE}-entrypoint.sh ./
RUN chmod +x ./${SCOPE}-entrypoint.sh
RUN chmod +x ./entrypoint.sh
ENTRYPOINT ./${SCOPE}-entrypoint.sh
EXPOSE 3000 EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
CMD ["node", "server.js"]

13
apps/builder/.env.docker Normal file
View File

@ -0,0 +1,13 @@
# Don't edit this file
NEXT_PUBLIC_VIEWER_URL=DOCKER_PUBLIC_VIEWER_URL
NEXT_PUBLIC_SMTP_FROM=DOCKER_NEXT_PUBLIC_SMTP_FROM
NEXT_PUBLIC_SMTP_AUTH_DISABLED=DOCKER_NEXT_PUBLIC_SMTP_AUTH_DISABLED
NEXT_PUBLIC_GOOGLE_CLIENT_ID=DOCKER_NEXT_PUBLIC_GOOGLE_CLIENT_ID
NEXT_PUBLIC_GOOGLE_API_KEY=DOCKER_NEXT_PUBLIC_GOOGLE_API_KEY
NEXT_PUBLIC_GITHUB_CLIENT_ID=DOCKER_NEXT_PUBLIC_GITHUB_CLIENT_ID
NEXT_PUBLIC_GITLAB_CLIENT_ID=DOCKER_NEXT_PUBLIC_GITLAB_CLIENT_ID
NEXT_PUBLIC_GITLAB_NAME=DOCKER_NEXT_PUBLIC_GITLAB_NAME
NEXT_PUBLIC_FACEBOOK_CLIENT_ID=DOCKER_NEXT_PUBLIC_FACEBOOK_CLIENT_ID
NEXT_PUBLIC_GIPHY_API_KEY=DOCKER_NEXT_PUBLIC_GIPHY_API_KEY
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=DOCKER_NEXT_PUBLIC_STRIPE_PUBLIC_KEY
NEXT_PUBLIC_SENTRY_DSN=DOCKER_NEXT_PUBLIC_SENTRY_DSN

View File

@ -14,14 +14,15 @@ import { DividerWithText } from './DividerWithText'
import { SocialLoginButtons } from './SocialLoginButtons' import { SocialLoginButtons } from './SocialLoginButtons'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { NextChakraLink } from 'components/nextChakra/NextChakraLink' import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
import { isEmpty } from 'utils'
const hasNoAuthProvider = const hasNoAuthProvider =
(!process.env.NEXT_PUBLIC_SMTP_FROM || (isEmpty(process.env.NEXT_PUBLIC_SMTP_FROM) ||
process.env.NEXT_PUBLIC_SMTP_AUTH_DISABLED === 'true') && process.env.NEXT_PUBLIC_SMTP_AUTH_DISABLED === 'true') &&
!process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID && isEmpty(process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) &&
!process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID && isEmpty(process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID) &&
!process.env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID && isEmpty(process.env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID) &&
!process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID isEmpty(process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID)
type Props = { type Props = {
defaultEmail?: string defaultEmail?: string
@ -77,7 +78,7 @@ export const SignInForm = ({
return ( return (
<Stack spacing="4" w="330px"> <Stack spacing="4" w="330px">
<SocialLoginButtons /> <SocialLoginButtons />
{process.env.NEXT_PUBLIC_SMTP_FROM && {!isEmpty(process.env.NEXT_PUBLIC_SMTP_FROM) &&
process.env.NEXT_PUBLIC_SMTP_AUTH_DISABLED !== 'true' && ( process.env.NEXT_PUBLIC_SMTP_AUTH_DISABLED !== 'true' && (
<> <>
<DividerWithText mt="6">Or with your email</DividerWithText> <DividerWithText mt="6">Or with your email</DividerWithText>

View File

@ -5,6 +5,7 @@ import { useRouter } from 'next/router'
import React from 'react' import React from 'react'
import { stringify } from 'qs' import { stringify } from 'qs'
import { FacebookLogo, GoogleLogo, GitlabLogo } from 'assets/logos' import { FacebookLogo, GoogleLogo, GitlabLogo } from 'assets/logos'
import { isEmpty } from 'utils'
export const SocialLoginButtons = () => { export const SocialLoginButtons = () => {
const { query } = useRouter() const { query } = useRouter()
@ -32,7 +33,7 @@ export const SocialLoginButtons = () => {
return ( return (
<Stack> <Stack>
{process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID && ( {!isEmpty(process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID) && (
<Button <Button
leftIcon={<GithubIcon />} leftIcon={<GithubIcon />}
onClick={handleGitHubClick} onClick={handleGitHubClick}
@ -43,7 +44,7 @@ export const SocialLoginButtons = () => {
Continue with GitHub Continue with GitHub
</Button> </Button>
)} )}
{process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID && ( {!isEmpty(process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) && (
<Button <Button
leftIcon={<GoogleLogo />} leftIcon={<GoogleLogo />}
onClick={handleGoogleClick} onClick={handleGoogleClick}
@ -54,7 +55,7 @@ export const SocialLoginButtons = () => {
Continue with Google Continue with Google
</Button> </Button>
)} )}
{process.env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID && ( {!isEmpty(process.env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID) && (
<Button <Button
leftIcon={<FacebookLogo />} leftIcon={<FacebookLogo />}
onClick={handleFacebookClick} onClick={handleFacebookClick}
@ -65,7 +66,7 @@ export const SocialLoginButtons = () => {
Continue with Facebook Continue with Facebook
</Button> </Button>
)} )}
{process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID && ( {!isEmpty(process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID) && (
<Button <Button
leftIcon={<GitlabLogo />} leftIcon={<GitlabLogo />}
onClick={handleGitlabClick} onClick={handleGitlabClick}
@ -73,7 +74,10 @@ export const SocialLoginButtons = () => {
isLoading={['loading', 'authenticated'].includes(status)} isLoading={['loading', 'authenticated'].includes(status)}
variant="outline" variant="outline"
> >
Continue with {process.env.NEXT_PUBLIC_GITLAB_NAME || 'GitLab'} Continue with{' '}
{isEmpty(process.env.NEXT_PUBLIC_GITLAB_NAME)
? 'GitLab'
: process.env.NEXT_PUBLIC_GITLAB_NAME}
</Button> </Button>
)} )}
</Stack> </Stack>

View File

@ -5,6 +5,7 @@ import { BubbleParams } from 'typebot-js'
import { parseInitBubbleCode, typebotJsHtml } from '../params' import { parseInitBubbleCode, typebotJsHtml } from '../params'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { isEmpty } from 'utils'
type ChatEmbedCodeProps = { type ChatEmbedCodeProps = {
withStarterVariables?: boolean withStarterVariables?: boolean
@ -20,8 +21,9 @@ export const ChatEmbedCode = ({
const snippet = prettier.format( const snippet = prettier.format(
createSnippet({ createSnippet({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
button, button,
proactiveMessage, proactiveMessage,

View File

@ -5,6 +5,7 @@ import { parseInitContainerCode, typebotJsHtml } from '../params'
import { IframeParams } from 'typebot-js' import { IframeParams } from 'typebot-js'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { isEmpty } from 'utils'
type ContainerEmbedCodeProps = { type ContainerEmbedCodeProps = {
widthLabel: string widthLabel: string
@ -22,8 +23,9 @@ export const ContainerEmbedCode = ({
const snippet = prettier.format( const snippet = prettier.format(
parseSnippet({ parseSnippet({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
heightLabel, heightLabel,
widthLabel, widthLabel,

View File

@ -1,6 +1,7 @@
import { FlexProps } from '@chakra-ui/react' import { FlexProps } from '@chakra-ui/react'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { isEmpty } from 'utils'
type Props = { type Props = {
widthLabel: string widthLabel: string
@ -13,8 +14,9 @@ export const IframeEmbedCode = ({
}: Props & FlexProps) => { }: Props & FlexProps) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const src = `${ const src = `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${typebot?.publicId}` }/${typebot?.publicId}`
const code = `<iframe src="${src}" width="${widthLabel}" height="${heightLabel}" />` const code = `<iframe src="${src}" width="${widthLabel}" height="${heightLabel}" />`

View File

@ -4,6 +4,7 @@ import { useTypebot } from 'contexts/TypebotContext'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { PopupParams } from 'typebot-js' import { PopupParams } from 'typebot-js'
import { isEmpty } from 'utils'
import { parseInitPopupCode, typebotJsHtml } from '../params' import { parseInitPopupCode, typebotJsHtml } from '../params'
type PopupEmbedCodeProps = { type PopupEmbedCodeProps = {
@ -17,8 +18,9 @@ export const PopupEmbedCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const snippet = prettier.format( const snippet = prettier.format(
createSnippet({ createSnippet({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
delay, delay,
}), }),

View File

@ -10,6 +10,7 @@ import parserBabel from 'prettier/parser-babel'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { isEmpty } from 'utils'
type StandardReactDivProps = { widthLabel: string; heightLabel: string } type StandardReactDivProps = { widthLabel: string; heightLabel: string }
export const StandardReactDiv = ({ export const StandardReactDiv = ({
@ -20,8 +21,9 @@ export const StandardReactDiv = ({
const snippet = prettier.format( const snippet = prettier.format(
parseContainerSnippet({ parseContainerSnippet({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
heightLabel, heightLabel,
widthLabel, widthLabel,
@ -71,8 +73,9 @@ export const PopupReactCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const snippet = prettier.format( const snippet = prettier.format(
parsePopupSnippet({ parsePopupSnippet({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
delay, delay,
}), }),
@ -121,8 +124,9 @@ export const ChatReactCode = ({
const snippet = prettier.format( const snippet = prettier.format(
parseBubbleSnippet({ parseBubbleSnippet({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
button, button,
proactiveMessage, proactiveMessage,

View File

@ -18,6 +18,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { createCustomDomain } from 'services/user' import { createCustomDomain } from 'services/user'
import { isEmpty } from 'utils'
const hostnameRegex = const hostnameRegex =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
@ -119,8 +120,11 @@ export const CustomDomainModal = ({
</Stack> </Stack>
<Stack> <Stack>
<Text fontWeight="bold">Value</Text> <Text fontWeight="bold">Value</Text>
<Text>{process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? <Text>
process.env.NEXT_PUBLIC_VIEWER_URL}</Text> {isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL}
</Text>
</Stack> </Stack>
</HStack> </HStack>
) : ( ) : (

View File

@ -11,6 +11,7 @@ import { PopupEmbedSettings } from 'components/share/codeSnippets/Popup/EmbedSet
import { CodeEditor } from 'components/shared/CodeEditor' import { CodeEditor } from 'components/shared/CodeEditor'
import { useState } from 'react' import { useState } from 'react'
import { BubbleParams } from 'typebot-js' import { BubbleParams } from 'typebot-js'
import { isEmpty } from 'utils'
import { ModalProps } from '../../EmbedButton' import { ModalProps } from '../../EmbedButton'
type GtmInstructionsProps = { type GtmInstructionsProps = {
@ -40,8 +41,9 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
const jsCode = parseInitContainerCode({ const jsCode = parseInitContainerCode({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${publicId}`, }/${publicId}`,
}) })
const headCode = `${typebotJsHtml} const headCode = `${typebotJsHtml}

View File

@ -16,6 +16,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { CopyButton } from 'components/shared/buttons/CopyButton' import { CopyButton } from 'components/shared/buttons/CopyButton'
import { PublishFirstInfo } from 'components/shared/Info' import { PublishFirstInfo } from 'components/shared/Info'
import { isEmpty } from 'utils'
import { ModalProps } from '../EmbedButton' import { ModalProps } from '../EmbedButton'
export const NotionModal = ({ export const NotionModal = ({
@ -45,15 +46,17 @@ export const NotionModal = ({
pr="4.5rem" pr="4.5rem"
type={'text'} type={'text'}
defaultValue={`${ defaultValue={`${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${publicId}`} }/${publicId}`}
/> />
<InputRightElement width="4.5rem"> <InputRightElement width="4.5rem">
<CopyButton <CopyButton
textToCopy={`${ textToCopy={`${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${publicId}`} }/${publicId}`}
/> />
</InputRightElement> </InputRightElement>

View File

@ -14,6 +14,7 @@ import { BubbleParams } from 'typebot-js'
import { ModalProps } from '../../EmbedButton' import { ModalProps } from '../../EmbedButton'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { isEmpty } from 'utils'
type ShopifyInstructionsProps = { type ShopifyInstructionsProps = {
type: 'standard' | 'popup' | 'bubble' type: 'standard' | 'popup' | 'bubble'
@ -45,8 +46,9 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
const jsCode = parseInitContainerCode({ const jsCode = parseInitContainerCode({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${publicId}`, }/${publicId}`,
}) })
const headCode = prettier.format( const headCode = prettier.format(

View File

@ -17,6 +17,7 @@ import {
import { ExternalLinkIcon } from 'assets/icons' import { ExternalLinkIcon } from 'assets/icons'
import { CopyButton } from 'components/shared/buttons/CopyButton' import { CopyButton } from 'components/shared/buttons/CopyButton'
import { PublishFirstInfo } from 'components/shared/Info' import { PublishFirstInfo } from 'components/shared/Info'
import { isEmpty } from 'utils'
import { ModalProps } from '../EmbedButton' import { ModalProps } from '../EmbedButton'
export const WordpressModal = ({ export const WordpressModal = ({
@ -54,15 +55,17 @@ export const WordpressModal = ({
pr="4.5rem" pr="4.5rem"
type={'text'} type={'text'}
defaultValue={`${ defaultValue={`${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${publicId}`} }/${publicId}`}
/> />
<InputRightElement width="4.5rem"> <InputRightElement width="4.5rem">
<CopyButton <CopyButton
textToCopy={`${ textToCopy={`${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/${publicId}`} }/${publicId}`}
/> />
</InputRightElement> </InputRightElement>

View File

@ -9,6 +9,7 @@ import { useDebouncedCallback } from 'use-debounce'
import { linter } from '@codemirror/lint' import { linter } from '@codemirror/lint'
import { VariablesButton } from './buttons/VariablesButton' import { VariablesButton } from './buttons/VariablesButton'
import { Variable } from 'models' import { Variable } from 'models'
import { isEmpty } from 'utils'
const linterExtension = linter(jsonParseLinter()) const linterExtension = linter(jsonParseLinter())
@ -40,7 +41,7 @@ export const CodeEditor = ({
setPlainTextValue(value) setPlainTextValue(value)
onChange && onChange(value) onChange && onChange(value)
}, },
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0
) )
useEffect( useEffect(

View File

@ -16,6 +16,7 @@ import { useTypebot } from 'contexts/TypebotContext'
import { BaseEmoji, emojiIndex } from 'emoji-mart' import { BaseEmoji, emojiIndex } from 'emoji-mart'
import { emojis } from './emojis' import { emojis } from './emojis'
import { Input } from '../Textbox/Input' import { Input } from '../Textbox/Input'
import { isEmpty } from 'utils'
type Props = { type Props = {
url?: string url?: string
@ -182,7 +183,7 @@ const EmojiContent = ({
} }
const GiphyContent = ({ onNewUrl }: ContentProps) => { const GiphyContent = ({ onNewUrl }: ContentProps) => {
if (!process.env.NEXT_PUBLIC_GIPHY_API_KEY) if (isEmpty(process.env.NEXT_PUBLIC_GIPHY_API_KEY))
return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text> return <Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text>
return ( return (
<SearchContextManager <SearchContextManager

View File

@ -11,6 +11,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useState, useRef, useEffect, ChangeEvent } from 'react' import { useState, useRef, useEffect, ChangeEvent } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { isEmpty } from 'utils'
type Props = { type Props = {
selectedItem?: string selectedItem?: string
@ -31,7 +32,7 @@ export const SearchableDropdown = ({
const debounced = useDebouncedCallback( const debounced = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
onValueChange ? onValueChange : () => {}, onValueChange ? onValueChange : () => {},
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0
) )
const [filteredItems, setFilteredItems] = useState([ const [filteredItems, setFilteredItems] = useState([
...items ...items

View File

@ -8,6 +8,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { isEmpty } from 'utils'
export const SmartNumberInput = ({ export const SmartNumberInput = ({
value, value,
@ -22,7 +23,7 @@ export const SmartNumberInput = ({
const [currentValue, setCurrentValue] = useState(value?.toString() ?? '') const [currentValue, setCurrentValue] = useState(value?.toString() ?? '')
const debounced = useDebouncedCallback( const debounced = useDebouncedCallback(
onValueChange, onValueChange,
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0
) )
useEffect( useEffect(

View File

@ -5,6 +5,7 @@ import React, { useEffect, useState } from 'react'
import { isCloudProdInstance } from 'services/utils' import { isCloudProdInstance } from 'services/utils'
import { planToReadable } from 'services/workspace' import { planToReadable } from 'services/workspace'
import { initBubble } from 'typebot-js' import { initBubble } from 'typebot-js'
import { isEmpty } from 'utils'
export const SupportBubble = () => { export const SupportBubble = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
@ -22,8 +23,9 @@ export const SupportBubble = () => {
setLocalUserId(user?.id) setLocalUserId(user?.id)
initBubble({ initBubble({
url: `${ url: `${
process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL)
process.env.NEXT_PUBLIC_VIEWER_URL ? process.env.NEXT_PUBLIC_VIEWER_URL
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL
}/typebot-support`, }/typebot-support`,
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
button: { button: {

View File

@ -7,6 +7,7 @@ import {
import { Variable } from 'models' import { Variable } from 'models'
import React, { ChangeEvent, useEffect, useRef, useState } from 'react' import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { isEmpty } from 'utils'
import { VariablesButton } from '../buttons/VariablesButton' import { VariablesButton } from '../buttons/VariablesButton'
export type TextBoxProps = { export type TextBoxProps = {
@ -35,7 +36,7 @@ export const TextBox = ({
(value) => { (value) => {
onChange(value) onChange(value)
}, },
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0
) )
useEffect(() => { useEffect(() => {

View File

@ -17,7 +17,7 @@ import cuid from 'cuid'
import { Variable } from 'models' import { Variable } from 'models'
import React, { useState, useRef, ChangeEvent, useEffect } from 'react' import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { byId, isNotDefined } from 'utils' import { byId, isEmpty, isNotDefined } from 'utils'
type Props = { type Props = {
initialVariableId?: string initialVariableId?: string
@ -47,7 +47,7 @@ export const VariableSearchInput = ({
const variable = variables.find((v) => v.name === value) const variable = variables.find((v) => v.name === value)
if (variable) onSelectVariable(variable) if (variable) onSelectVariable(variable)
}, },
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0
) )
const [filteredItems, setFilteredItems] = useState<Variable[]>( const [filteredItems, setFilteredItems] = useState<Variable[]>(
variables ?? [] variables ?? []

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Text, HStack } from '@chakra-ui/react' import { Text, HStack } from '@chakra-ui/react'
import { SearchableDropdown } from '../../../shared/SearchableDropdown' import { SearchableDropdown } from '../../../shared/SearchableDropdown'
import { isEmpty } from 'utils'
type FontSelectorProps = { type FontSelectorProps = {
activeFont?: string activeFont?: string
@ -19,7 +20,7 @@ export const FontSelector = ({
}, []) }, [])
const fetchPopularFonts = async () => { const fetchPopularFonts = async () => {
if (!process.env.NEXT_PUBLIC_GOOGLE_API_KEY) return [] if (isEmpty(process.env.NEXT_PUBLIC_GOOGLE_API_KEY)) return []
const response = await fetch( const response = await fetch(
`https://www.googleapis.com/webfonts/v1/webfonts?key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&sort=popularity` `https://www.googleapis.com/webfonts/v1/webfonts?key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&sort=popularity`
) )

View File

@ -30,7 +30,7 @@ import {
} from 'services/typebots/typebots' } from 'services/typebots/typebots'
import { fetcher, preventUserFromRefreshing } from 'services/utils' import { fetcher, preventUserFromRefreshing } from 'services/utils'
import useSWR from 'swr' import useSWR from 'swr'
import { isDefined, isNotDefined, omit } from 'utils' import { isDefined, isEmpty, isNotDefined, omit } from 'utils'
import { BlocksActions, blocksActions } from './actions/blocks' import { BlocksActions, blocksActions } from './actions/blocks'
import { stepsAction, StepsActions } from './actions/steps' import { stepsAction, StepsActions } from './actions/steps'
import { variablesAction, VariablesActions } from './actions/variables' import { variablesAction, VariablesActions } from './actions/variables'
@ -410,7 +410,7 @@ export const useFetchedTypebot = ({
}, },
Error Error
>(`/api/typebots/${typebotId}`, fetcher, { >(`/api/typebots/${typebotId}`, fetcher, {
dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined, dedupingInterval: isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? undefined : 0,
}) })
if (error) onError(error) if (error) onError(error)
return { return {

View File

@ -10,10 +10,11 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { withSentry } from '@sentry/nextjs' import { withSentry } from '@sentry/nextjs'
import { CustomAdapter } from './adapter' import { CustomAdapter } from './adapter'
import { User } from 'db' import { User } from 'db'
import { isNotEmpty } from 'utils'
const providers: Provider[] = [] const providers: Provider[] = []
if (process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID) if (isNotEmpty(process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID))
providers.push( providers.push(
GitHubProvider({ GitHubProvider({
clientId: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID, clientId: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID,
@ -22,7 +23,7 @@ if (process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID)
) )
if ( if (
process.env.NEXT_PUBLIC_SMTP_FROM && isNotEmpty(process.env.NEXT_PUBLIC_SMTP_FROM) &&
process.env.NEXT_PUBLIC_SMTP_AUTH_DISABLED !== 'true' process.env.NEXT_PUBLIC_SMTP_AUTH_DISABLED !== 'true'
) )
providers.push( providers.push(
@ -40,8 +41,8 @@ if (
) )
if ( if (
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID && isNotEmpty(process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) &&
process.env.GOOGLE_CLIENT_SECRET isNotEmpty(process.env.GOOGLE_CLIENT_SECRET)
) )
providers.push( providers.push(
GoogleProvider({ GoogleProvider({
@ -51,8 +52,8 @@ if (
) )
if ( if (
process.env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID && isNotEmpty(process.env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID) &&
process.env.FACEBOOK_CLIENT_SECRET isNotEmpty(process.env.FACEBOOK_CLIENT_SECRET)
) )
providers.push( providers.push(
FacebookProvider({ FacebookProvider({
@ -62,8 +63,8 @@ if (
) )
if ( if (
process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID && isNotEmpty(process.env.NEXT_PUBLIC_GITLAB_CLIENT_ID) &&
process.env.GITLAB_CLIENT_SECRET isNotEmpty(process.env.GITLAB_CLIENT_SECRET)
) { ) {
const BASE_URL = process.env.GITLAB_BASE_URL || 'https://gitlab.com' const BASE_URL = process.env.GITLAB_BASE_URL || 'https://gitlab.com'
providers.push( providers.push(

View File

@ -9,6 +9,7 @@ import { getAuthenticatedUser } from 'services/api/utils'
import { import {
badRequest, badRequest,
forbidden, forbidden,
isEmpty,
isNotDefined, isNotDefined,
methodNotAllowed, methodNotAllowed,
notAuthenticated, notAuthenticated,
@ -66,7 +67,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await prisma.invitation.create({ await prisma.invitation.create({
data: { email: email.toLowerCase(), type, typebotId }, data: { email: email.toLowerCase(), type, typebotId },
}) })
if (isNotDefined(process.env.NEXT_PUBLIC_E2E_TEST)) if (isEmpty(process.env.NEXT_PUBLIC_E2E_TEST))
await sendEmailNotification({ await sendEmailNotification({
to: email, to: email,
subject: "You've been invited to collaborate 🤝", subject: "You've been invited to collaborate 🤝",

View File

@ -1,4 +1,5 @@
import { CollaborationType, Prisma, User, WorkspaceRole } from 'db' import { CollaborationType, Prisma, User, WorkspaceRole } from 'db'
import { isNotEmpty } from 'utils'
const parseWhereFilter = ( const parseWhereFilter = (
typebotIds: string[] | string, typebotIds: string[] | string,
@ -19,7 +20,7 @@ const parseWhereFilter = (
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds }, id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
workspace: workspace:
(type === 'read' && user.email === process.env.ADMIN_EMAIL) || (type === 'read' && user.email === process.env.ADMIN_EMAIL) ||
process.env.NEXT_PUBLIC_E2E_TEST isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST)
? undefined ? undefined
: { : {
members: { members: {

View File

@ -2,7 +2,7 @@ import { DashboardFolder } from 'db'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from './utils' import { fetcher } from './utils'
import { stringify } from 'qs' import { stringify } from 'qs'
import { sendRequest } from 'utils' import { isNotEmpty, sendRequest } from 'utils'
export const useFolders = ({ export const useFolders = ({
parentId, parentId,
@ -17,7 +17,11 @@ export const useFolders = ({
const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>( const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>(
workspaceId ? `/api/folders?${params}` : null, workspaceId ? `/api/folders?${params}` : null,
fetcher, fetcher,
{ dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined } {
dedupingInterval: isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST)
? 0
: undefined,
}
) )
if (error) onError(error) if (error) onError(error)
return { return {

View File

@ -1,6 +1,6 @@
import { User } from 'db' import { User } from 'db'
import { loadStripe } from '@stripe/stripe-js/pure' import { loadStripe } from '@stripe/stripe-js/pure'
import { sendRequest } from 'utils' import { isEmpty, sendRequest } from 'utils'
type Props = { type Props = {
user: User user: User
@ -10,7 +10,7 @@ type Props = {
} }
export const pay = async ({ user, currency, plan, workspaceId }: Props) => { export const pay = async ({ user, currency, plan, workspaceId }: Props) => {
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY) if (isEmpty(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY))
throw new Error('NEXT_PUBLIC_STRIPE_PUBLIC_KEY is missing in env') throw new Error('NEXT_PUBLIC_STRIPE_PUBLIC_KEY is missing in env')
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY) const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY)
const { data, error } = await sendRequest<{ sessionId: string }>({ const { data, error } = await sendRequest<{ sessionId: string }>({

View File

@ -1,7 +1,7 @@
import { CollaborationType, Invitation } from 'db' import { CollaborationType, Invitation } from 'db'
import { fetcher } from 'services/utils' import { fetcher } from 'services/utils'
import useSWR from 'swr' import useSWR from 'swr'
import { sendRequest } from 'utils' import { isNotEmpty, sendRequest } from 'utils'
export const useInvitations = ({ export const useInvitations = ({
typebotId, typebotId,
@ -13,7 +13,11 @@ export const useInvitations = ({
const { data, error, mutate } = useSWR<{ invitations: Invitation[] }, Error>( const { data, error, mutate } = useSWR<{ invitations: Invitation[] }, Error>(
typebotId ? `/api/typebots/${typebotId}/invitations` : null, typebotId ? `/api/typebots/${typebotId}/invitations` : null,
fetcher, fetcher,
{ dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined } {
dedupingInterval: isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST)
? 0
: undefined,
}
) )
if (error) onError(error) if (error) onError(error)
return { return {

View File

@ -44,6 +44,7 @@ import useSWR from 'swr'
import { fetcher, toKebabCase } from '../utils' import { fetcher, toKebabCase } from '../utils'
import { import {
isBubbleStepType, isBubbleStepType,
isNotEmpty,
isWebhookStep, isWebhookStep,
omit, omit,
stepHasItems, stepHasItems,
@ -80,7 +81,9 @@ export const useTypebots = ({
{ typebots: TypebotInDashboard[] }, { typebots: TypebotInDashboard[] },
Error Error
>(workspaceId ? `/api/typebots?${params}` : null, fetcher, { >(workspaceId ? `/api/typebots?${params}` : null, fetcher, {
dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined, dedupingInterval: isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST)
? 0
: undefined,
}) })
if (error) onError(error) if (error) onError(error)
return { return {

View File

@ -1,7 +1,7 @@
import { MemberInWorkspace, WorkspaceInvitation } from 'db' import { MemberInWorkspace, WorkspaceInvitation } from 'db'
import { fetcher } from 'services/utils' import { fetcher } from 'services/utils'
import useSWR from 'swr' import useSWR from 'swr'
import { sendRequest } from 'utils' import { isEmpty, sendRequest } from 'utils'
export type Member = MemberInWorkspace & { export type Member = MemberInWorkspace & {
name: string | null name: string | null
@ -14,7 +14,7 @@ export const useMembers = ({ workspaceId }: { workspaceId?: string }) => {
{ members: Member[]; invitations: WorkspaceInvitation[] }, { members: Member[]; invitations: WorkspaceInvitation[] },
Error Error
>(workspaceId ? `/api/workspaces/${workspaceId}/members` : null, fetcher, { >(workspaceId ? `/api/workspaces/${workspaceId}/members` : null, fetcher, {
dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined, dedupingInterval: isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? undefined : 0,
}) })
return { return {
members: data?.members, members: data?.members,

View File

@ -18,17 +18,13 @@ You need a server with Docker installed. If your server doesn't come with Docker
On your server: On your server:
1. Clone the repo: 1. Download the latest `docker-compose.yml` file:
```sh ```sh
git clone https://github.com/baptistearno/typebot.io.git wget https://raw.githubusercontent.com/baptisteArno/typebot.io/latest/docker-compose.yml
``` ```
2. Set up environment variables 2. Open the file and set the environment variables for both `typebot-builder` and `typebot-viewer`
Copy `apps/builder/.env.production.example` to `apps/builder/.env.production`
Copy `apps/viewer/.env.production.example` to `apps/viewer/.env.production`
Check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration) to add your environment variables Check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration) to add your environment variables
@ -45,4 +41,4 @@ On your server:
- Start the builder on port 8080 - Start the builder on port 8080
- Start the viewer on port 8081 - Start the viewer on port 8081
You should see the login screen if you navigate to `http://{hostname}:8080`. Login with the `${ADMIN_EMAIL}` to have access to a Pro account automatically. You should see the login screen if you navigate to `http://{hostname}:8080`. Login with the `${ADMIN_EMAIL}` to have access to a Team plan workspace automatically.

3
apps/viewer/.env.docker Normal file
View File

@ -0,0 +1,3 @@
NEXT_PUBLIC_VIEWER_URL=DOCKER_PUBLIC_VIEWER_URL
NEXT_PUBLIC_SMTP_FROM=DOCKER_NEXT_PUBLIC_SMTP_FROM
NEXT_PUBLIC_GOOGLE_CLIENT_ID=DOCKER_NEXT_PUBLIC_GOOGLE_CLIENT_ID

View File

@ -1,4 +1,3 @@
DATABASE_URL=postgresql://postgres:typebot@db:5432/typebot DATABASE_URL=postgresql://postgres:typebot@db:5432/typebot
NEXT_PUBLIC_VIEWER_URL=http://localhost:8081 NEXT_PUBLIC_VIEWER_URL=http://localhost:8081
ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6 ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6
ADMIN_EMAIL=contact@baptiste-arnaud.fr

View File

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { isEmpty } from 'utils'
export const ErrorPage = ({ error }: { error: Error }) => { export const ErrorPage = ({ error }: { error: Error }) => {
return ( return (
@ -11,7 +12,7 @@ export const ErrorPage = ({ error }: { error: Error }) => {
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
{!process.env.NEXT_PUBLIC_VIEWER_URL ? ( {isEmpty(process.env.NEXT_PUBLIC_VIEWER_URL) ? (
<> <>
<h1 style={{ fontWeight: 'bold', fontSize: '30px' }}> <h1 style={{ fontWeight: 'bold', fontSize: '30px' }}>
NEXT_PUBLIC_VIEWER_URL is missing NEXT_PUBLIC_VIEWER_URL is missing

View File

@ -1,4 +1,5 @@
import { CollaborationType, Prisma, User, WorkspaceRole } from 'db' import { CollaborationType, Prisma, User, WorkspaceRole } from 'db'
import { isNotEmpty } from 'utils'
const parseWhereFilter = ( const parseWhereFilter = (
typebotIds: string[] | string, typebotIds: string[] | string,
@ -19,7 +20,7 @@ const parseWhereFilter = (
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds }, id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
workspace: workspace:
(type === 'read' && user.email === process.env.ADMIN_EMAIL) || (type === 'read' && user.email === process.env.ADMIN_EMAIL) ||
process.env.NEXT_PUBLIC_E2E_TEST isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST)
? undefined ? undefined
: { : {
members: { members: {

12
builder-entrypoint.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/bash
./entrypoint.sh
./node_modules/.bin/prisma generate;
echo 'Waiting 5s for db to be ready...';
sleep 5;
./node_modules/.bin/prisma migrate deploy;
node server.js;

46
docker-compose.build.yml Normal file
View File

@ -0,0 +1,46 @@
version: '3.9'
services:
typebot-db:
image: postgres:13
restart: always
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=typebot
- POSTGRES_PASSWORD=typebot
typebot-builder:
build:
context: .
args:
- SCOPE=builder
restart: always
depends_on:
- db
ports:
- '8080:3000'
extra_hosts:
- 'host.docker.internal:host-gateway'
# See https://docs.typebot.io/self-hosting/configuration for more configuration options
environment:
- DATABASE_URL=postgresql://postgres:typebot@typebot-db:5432/typebot
- NEXTAUTH_URL=http://localhost:8080
- NEXT_PUBLIC_VIEWER_URL=http://localhost:8081
- ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6
- ADMIN_EMAIL=me@email.com
- NEXTAUTH_URL_INTERNAL=http://host.docker.internal:8080
- NEXT_PUBLIC_GOOGLE_CLIENT_ID=fnejwkn
typebot-viewer:
build:
context: .
args:
- SCOPE=viewer
restart: always
ports:
- '8081:3000'
# See https://docs.typebot.io/self-hosting/configuration for more configuration options
environment:
- DATABASE_URL=postgresql://postgres:typebot@typebot-db:5432/typebot
- NEXT_PUBLIC_VIEWER_URL=http://localhost:8081
- ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6
volumes:
db_data:

View File

@ -1,6 +1,6 @@
version: '3.9' version: '3.9'
services: services:
db: typebot-db:
image: postgres:13 image: postgres:13
restart: always restart: always
volumes: volumes:
@ -8,42 +8,35 @@ services:
environment: environment:
- POSTGRES_DB=typebot - POSTGRES_DB=typebot
- POSTGRES_PASSWORD=typebot - POSTGRES_PASSWORD=typebot
builder: typebot-builder:
depends_on: image: baptistearno/typebot-builder:latest
- db
build: build:
context: . context: .
args: args:
- SCOPE=builder - SCOPE=builder
restart: always
depends_on:
- db
ports: ports:
- '8080:3000' - '8080:3000'
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - 'host.docker.internal:host-gateway'
env_file: # See https://docs.typebot.io/self-hosting/configuration for more configuration options
- './apps/builder/.env.production' environment:
entrypoint: > - DATABASE_URL=postgresql://postgres:typebot@typebot-db:5432/typebot
/bin/sh -c " - NEXTAUTH_URL=http://localhost:8080
./node_modules/.bin/prisma generate; - NEXT_PUBLIC_VIEWER_URL=http://localhost:8081
echo 'Waiting 5s for db to be ready...'; - ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6
sleep 5; - ADMIN_EMAIL=me@email.com
./node_modules/.bin/prisma migrate deploy; typebot-viewer:
node server.js;" image: baptistearno/typebot-viewer:latest
viewer:
depends_on:
- db
restart: always restart: always
build:
context: .
args:
- SCOPE=viewer
ports: ports:
- '8081:3000' - '8081:3000'
env_file: # See https://docs.typebot.io/self-hosting/configuration for more configuration options
- './apps/viewer/.env.production' environment:
entrypoint: > - DATABASE_URL=postgresql://postgres:typebot@typebot-db:5432/typebot
/bin/sh -c " - NEXT_PUBLIC_VIEWER_URL=http://localhost:8081
./node_modules/.bin/prisma generate; - ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6
node server.js;"
volumes: volumes:
db_data: db_data:
s3_data:

35
entrypoint.sh Normal file
View File

@ -0,0 +1,35 @@
#!/bin/bash
# This entrypoint inject variables at runtime.
# See https://raphaelpralat.medium.com/system-environment-variables-in-next-js-with-docker-1f0754e04cde
# no verbose
set +x
# config
envFilename='.env.production'
nextFolder='./.next/'
function apply_path {
# read all config file
while read line; do
# no comment or not empty
if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then
continue
fi
# split
configName="$(cut -d'=' -f1 <<<"$line")"
configValue="$(cut -d'=' -f2 <<<"$line")" # get system env
envValue=$(env | grep "^$configName=" | grep -oe '[^=]*$');
if [ -n "$configValue" ] && [ -n "$envValue" ]; then
echo "Injecting ${configName}..."
fi
# replace all
find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#$envValue#g"
done < $envFilename
}
apply_path
exec "$@"

View File

@ -21,6 +21,7 @@ import {
} from 'models' } from 'models'
import { Log } from 'db' import { Log } from 'db'
import { LiteBadge } from './LiteBadge' import { LiteBadge } from './LiteBadge'
import { isEmpty } from 'utils'
export type TypebotViewerProps = { export type TypebotViewerProps = {
typebot: PublicTypebot typebot: PublicTypebot
@ -66,7 +67,7 @@ export const TypebotViewer = ({
const handleCompleted = () => onCompleted && onCompleted() const handleCompleted = () => onCompleted && onCompleted()
if (!apiHost) if (isEmpty(apiHost))
return <p>process.env.NEXT_PUBLIC_VIEWER_URL is missing in env</p> return <p>process.env.NEXT_PUBLIC_VIEWER_URL is missing in env</p>
return ( return (
<Frame <Frame

View File

@ -61,6 +61,12 @@ export const isNotDefined = <T>(
value: T | undefined | null value: T | undefined | null
): value is undefined | null => value === undefined || value === null ): value is undefined | null => value === undefined || value === null
export const isEmpty = (value: string | undefined | null): value is undefined =>
value === undefined || value === null || value === ''
export const isNotEmpty = (value: string | undefined | null): value is string =>
value !== undefined && value !== null && value !== ''
export const isInputStep = (step: Step): step is InputStep => export const isInputStep = (step: Step): step is InputStep =>
(Object.values(InputStepType) as string[]).includes(step.type) (Object.values(InputStepType) as string[]).includes(step.type)

7
viewer-entrypoint.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
./entrypoint.sh
./node_modules/.bin/prisma generate;
node server.js;