2
0

fix(docker): 🐛 Runtime public environment

This commit is contained in:
Baptiste Arnaud
2022-06-21 10:36:41 +02:00
parent ea765640cf
commit e03fe9f7d9
54 changed files with 349 additions and 164 deletions

2
.gitignore vendored
View File

@@ -24,3 +24,5 @@ firebaseServiceAccount.json
tags tags
dump.sql dump.sql
__env.js

View File

@@ -36,10 +36,10 @@ COPY --from=builder /app/apps/${SCOPE}/.next/static ./.next/static
COPY --from=builder /app/apps/${SCOPE}/.env.docker ./.env.production 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 env.sh ./
COPY ${SCOPE}-entrypoint.sh ./ COPY ${SCOPE}-entrypoint.sh ./
RUN chmod +x ./${SCOPE}-entrypoint.sh RUN chmod +x ./${SCOPE}-entrypoint.sh
RUN chmod +x ./entrypoint.sh RUN chmod +x ./env.sh
ENTRYPOINT ./${SCOPE}-entrypoint.sh ENTRYPOINT ./${SCOPE}-entrypoint.sh
EXPOSE 3000 EXPOSE 3000

View File

@@ -1,7 +1,7 @@
# Don't edit this file # Don't edit this file
NEXT_PUBLIC_VIEWER_URL=DOCKER_NEXT_PUBLIC_VIEWER_URL NEXT_PUBLIC_VIEWER_URL=
NEXT_PUBLIC_SMTP_FROM=DOCKER_NEXT_PUBLIC_SMTP_FROM NEXT_PUBLIC_SMTP_FROM=
NEXT_PUBLIC_GOOGLE_API_KEY=DOCKER_NEXT_PUBLIC_GOOGLE_API_KEY NEXT_PUBLIC_GOOGLE_API_KEY=
NEXT_PUBLIC_GIPHY_API_KEY=DOCKER_NEXT_PUBLIC_GIPHY_API_KEY NEXT_PUBLIC_GIPHY_API_KEY=
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=DOCKER_NEXT_PUBLIC_STRIPE_PUBLIC_KEY NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
NEXT_PUBLIC_SENTRY_DSN=DOCKER_NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN=

View File

@@ -20,6 +20,7 @@ import { isDefined, isNotDefined } from 'utils'
import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown' import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown'
import { EditableUrl } from './EditableUrl' import { EditableUrl } from './EditableUrl'
import { integrationsList } from './integrations/EmbedButton' import { integrationsList } from './integrations/EmbedButton'
import { env } from 'utils'
export const ShareContent = () => { export const ShareContent = () => {
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
@@ -58,9 +59,7 @@ export const ShareContent = () => {
</Heading> </Heading>
{typebot && ( {typebot && (
<EditableUrl <EditableUrl
hostname={ hostname={env('VIEWER_URL') ?? 'https://typebot.io'}
process.env.NEXT_PUBLIC_VIEWER_URL ?? 'https://typebot.io'
}
pathname={publicId} pathname={publicId}
onPathnameChange={handlePublicIdChange} onPathnameChange={handlePublicIdChange}
/> />

View File

@@ -5,7 +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' import { env, isEmpty } from 'utils'
type ChatEmbedCodeProps = { type ChatEmbedCodeProps = {
withStarterVariables?: boolean withStarterVariables?: boolean
@@ -21,9 +21,9 @@ export const ChatEmbedCode = ({
const snippet = prettier.format( const snippet = prettier.format(
createSnippet({ createSnippet({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
button, button,
proactiveMessage, proactiveMessage,

View File

@@ -5,7 +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' import { env, isEmpty } from 'utils'
type ContainerEmbedCodeProps = { type ContainerEmbedCodeProps = {
widthLabel: string widthLabel: string
@@ -23,9 +23,9 @@ export const ContainerEmbedCode = ({
const snippet = prettier.format( const snippet = prettier.format(
parseSnippet({ parseSnippet({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
heightLabel, heightLabel,
widthLabel, widthLabel,

View File

@@ -1,7 +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' import { env, isEmpty } from 'utils'
type Props = { type Props = {
widthLabel: string widthLabel: string
@@ -14,9 +14,9 @@ export const IframeEmbedCode = ({
}: Props & FlexProps) => { }: Props & FlexProps) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const src = `${ const src = `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('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,7 +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 { env, isEmpty } from 'utils'
import { parseInitPopupCode, typebotJsHtml } from '../params' import { parseInitPopupCode, typebotJsHtml } from '../params'
type PopupEmbedCodeProps = { type PopupEmbedCodeProps = {
@@ -18,9 +18,9 @@ export const PopupEmbedCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const snippet = prettier.format( const snippet = prettier.format(
createSnippet({ createSnippet({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
delay, delay,
}), }),

View File

@@ -10,7 +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' import { env, isEmpty } from 'utils'
type StandardReactDivProps = { widthLabel: string; heightLabel: string } type StandardReactDivProps = { widthLabel: string; heightLabel: string }
export const StandardReactDiv = ({ export const StandardReactDiv = ({
@@ -21,9 +21,9 @@ export const StandardReactDiv = ({
const snippet = prettier.format( const snippet = prettier.format(
parseContainerSnippet({ parseContainerSnippet({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
heightLabel, heightLabel,
widthLabel, widthLabel,
@@ -73,9 +73,9 @@ export const PopupReactCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
const snippet = prettier.format( const snippet = prettier.format(
parsePopupSnippet({ parsePopupSnippet({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
delay, delay,
}), }),
@@ -124,9 +124,9 @@ export const ChatReactCode = ({
const snippet = prettier.format( const snippet = prettier.format(
parseBubbleSnippet({ parseBubbleSnippet({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${typebot?.publicId}`, }/${typebot?.publicId}`,
button, button,
proactiveMessage, proactiveMessage,

View File

@@ -18,7 +18,7 @@ import {
import { useToast } from 'components/shared/hooks/useToast' import { useToast } from 'components/shared/hooks/useToast'
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' import { env, 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])$/
@@ -118,9 +118,9 @@ export const CustomDomainModal = ({
<Stack> <Stack>
<Text fontWeight="bold">Value</Text> <Text fontWeight="bold">Value</Text>
<Text> <Text>
{isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) {isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL} : env('VIEWER_INTERNAL_URL')}
</Text> </Text>
</Stack> </Stack>
</HStack> </HStack>

View File

@@ -11,7 +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 { env, isEmpty } from 'utils'
import { ModalProps } from '../../EmbedButton' import { ModalProps } from '../../EmbedButton'
type GtmInstructionsProps = { type GtmInstructionsProps = {
@@ -41,9 +41,9 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
const jsCode = parseInitContainerCode({ const jsCode = parseInitContainerCode({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${publicId}`, }/${publicId}`,
}) })
const headCode = `${typebotJsHtml} const headCode = `${typebotJsHtml}

View File

@@ -16,7 +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 { env, isEmpty } from 'utils'
import { ModalProps } from '../EmbedButton' import { ModalProps } from '../EmbedButton'
export const NotionModal = ({ export const NotionModal = ({
@@ -46,17 +46,17 @@ export const NotionModal = ({
pr="4.5rem" pr="4.5rem"
type={'text'} type={'text'}
defaultValue={`${ defaultValue={`${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
<InputRightElement width="4.5rem"> <InputRightElement width="4.5rem">
<CopyButton <CopyButton
textToCopy={`${ textToCopy={`${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
</InputRightElement> </InputRightElement>

View File

@@ -14,7 +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' import { env, isEmpty } from 'utils'
type ShopifyInstructionsProps = { type ShopifyInstructionsProps = {
type: 'standard' | 'popup' | 'bubble' type: 'standard' | 'popup' | 'bubble'
@@ -46,9 +46,9 @@ const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
const jsCode = parseInitContainerCode({ const jsCode = parseInitContainerCode({
url: `${ url: `${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${publicId}`, }/${publicId}`,
}) })
const headCode = prettier.format( const headCode = prettier.format(

View File

@@ -17,7 +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 { env, isEmpty } from 'utils'
import { ModalProps } from '../EmbedButton' import { ModalProps } from '../EmbedButton'
export const WordpressModal = ({ export const WordpressModal = ({
@@ -55,17 +55,17 @@ export const WordpressModal = ({
pr="4.5rem" pr="4.5rem"
type={'text'} type={'text'}
defaultValue={`${ defaultValue={`${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
<InputRightElement width="4.5rem"> <InputRightElement width="4.5rem">
<CopyButton <CopyButton
textToCopy={`${ textToCopy={`${
isEmpty(process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL) isEmpty(env('VIEWER_INTERNAL_URL'))
? process.env.NEXT_PUBLIC_VIEWER_URL ? env('VIEWER_URL')
: process.env.NEXT_PUBLIC_VIEWER_INTERNAL_URL : env('VIEWER_INTERNAL_URL')
}/${publicId}`} }/${publicId}`}
/> />
</InputRightElement> </InputRightElement>

View File

@@ -9,7 +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' import { env } from 'utils'
const linterExtension = linter(jsonParseLinter()) const linterExtension = linter(jsonParseLinter())
@@ -43,7 +43,7 @@ export const CodeEditor = ({
setPlainTextValue(value) setPlainTextValue(value)
onChange && onChange(value) onChange && onChange(value)
}, },
isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0 env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout
) )
useEffect( useEffect(

View File

@@ -12,6 +12,7 @@ import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
import { Input, Textarea } from 'components/shared/Textbox' import { Input, Textarea } from 'components/shared/Textbox'
import { CredentialsType, SendEmailOptions } from 'models' import { CredentialsType, SendEmailOptions } from 'models'
import React, { useState } from 'react' import React, { useState } from 'react'
import { env } from 'utils'
import { SmtpConfigModal } from './SmtpConfigModal' import { SmtpConfigModal } from './SmtpConfigModal'
type Props = { type Props = {
@@ -96,9 +97,9 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
currentCredentialsId={options.credentialsId} currentCredentialsId={options.credentialsId}
onCredentialsSelect={handleCredentialsSelect} onCredentialsSelect={handleCredentialsSelect}
onCreateNewClick={onOpen} onCreateNewClick={onOpen}
defaultCredentialLabel={process.env.NEXT_PUBLIC_SMTP_FROM?.match( defaultCredentialLabel={env('SMTP_FROM')
/\<(.*)\>/ ?.match(/\<(.*)\>/)
)?.pop()} ?.pop()}
refreshDropdownKey={refreshCredentialsKey} refreshDropdownKey={refreshCredentialsKey}
/> />
</Stack> </Stack>

View File

@@ -4,14 +4,13 @@ import { Grid, SearchContext } from '@giphy/react-components'
import { GiphyLogo } from 'assets/logos' import { GiphyLogo } from 'assets/logos'
import React, { useContext, useState, useEffect } from 'react' import React, { useContext, useState, useEffect } from 'react'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { env } from 'utils'
type GiphySearchProps = { type GiphySearchProps = {
onSubmit: (url: string) => void onSubmit: (url: string) => void
} }
const giphyFetch = new GiphyFetch( const giphyFetch = new GiphyFetch(env('GIPHY_API_KEY') as string)
process.env.NEXT_PUBLIC_GIPHY_API_KEY as string
)
export const GiphySearch = ({ onSubmit }: GiphySearchProps) => { export const GiphySearch = ({ onSubmit }: GiphySearchProps) => {
const { fetchGifs, searchKey, setSearch } = useContext(SearchContext) const { fetchGifs, searchKey, setSearch } = useContext(SearchContext)

View File

@@ -16,12 +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' import { env, isEmpty } from 'utils'
import getConfig from 'next/config'
const {
publicRuntimeConfig: { NEXT_PUBLIC_GIPHY_API_KEY },
} = getConfig()
type Props = { type Props = {
url?: string url?: string
@@ -188,10 +183,10 @@ const EmojiContent = ({
} }
const GiphyContent = ({ onNewUrl }: ContentProps) => { const GiphyContent = ({ onNewUrl }: ContentProps) => {
if (isEmpty(NEXT_PUBLIC_GIPHY_API_KEY)) if (isEmpty(env('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 apiKey={NEXT_PUBLIC_GIPHY_API_KEY}> <SearchContextManager apiKey={env('GIPHY_API_KEY') as string}>
<GiphySearch onSubmit={onNewUrl} /> <GiphySearch onSubmit={onNewUrl} />
</SearchContextManager> </SearchContextManager>
) )

View File

@@ -13,7 +13,7 @@ import {
import { Variable } from 'models' import { Variable } from 'models'
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' import { env } from 'utils'
import { VariablesButton } from './buttons/VariablesButton' import { VariablesButton } from './buttons/VariablesButton'
type Props = { type Props = {
@@ -38,7 +38,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 : () => {},
isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0 env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout
) )
const [filteredItems, setFilteredItems] = useState([ const [filteredItems, setFilteredItems] = useState([
...items ...items

View File

@@ -8,7 +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' import { env } from 'utils'
export const SmartNumberInput = ({ export const SmartNumberInput = ({
value, value,
@@ -23,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,
isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0 env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout
) )
useEffect( useEffect(

View File

@@ -7,7 +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 { env } from 'utils'
import { VariablesButton } from '../buttons/VariablesButton' import { VariablesButton } from '../buttons/VariablesButton'
export type TextBoxProps = { export type TextBoxProps = {
@@ -36,7 +36,7 @@ export const TextBox = ({
(value) => { (value) => {
onChange(value) onChange(value)
}, },
isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0 env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout
) )
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, isEmpty, isNotDefined } from 'utils' import { byId, env, 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)
}, },
isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0 env('E2E_TEST') === 'enabled' ? 0 : debounceTimeout
) )
const [filteredItems, setFilteredItems] = useState<Variable[]>( const [filteredItems, setFilteredItems] = useState<Variable[]>(
variables ?? [] variables ?? []

View File

@@ -1,8 +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' import { env, isEmpty } from 'utils'
import getConfig from 'next/config'
type FontSelectorProps = { type FontSelectorProps = {
activeFont?: string activeFont?: string
@@ -21,12 +20,11 @@ export const FontSelector = ({
}, []) }, [])
const fetchPopularFonts = async () => { const fetchPopularFonts = async () => {
const { if (isEmpty(env('GOOGLE_API_KEY'))) return []
publicRuntimeConfig: { NEXT_PUBLIC_GOOGLE_API_KEY },
} = getConfig()
if (isEmpty(NEXT_PUBLIC_GOOGLE_API_KEY)) return []
const response = await fetch( const response = await fetch(
`https://www.googleapis.com/webfonts/v1/webfonts?key=${NEXT_PUBLIC_GOOGLE_API_KEY}&sort=popularity` `https://www.googleapis.com/webfonts/v1/webfonts?key=${env(
'GOOGLE_API_KEY'
)}&sort=popularity`
) )
return (await response.json()).items.map( return (await response.json()).items.map(
(item: { family: string }) => item.family (item: { family: string }) => item.family

View File

@@ -29,7 +29,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, isEmpty, isNotDefined, omit } from 'utils' import { env, isDefined, isNotDefined, omit } from 'utils'
import { GroupsActions, groupsActions } from './actions/groups' import { GroupsActions, groupsActions } from './actions/groups'
import { blocksAction, BlocksActions } from './actions/blocks' import { blocksAction, BlocksActions } from './actions/blocks'
import { variablesAction, VariablesActions } from './actions/variables' import { variablesAction, VariablesActions } from './actions/variables'
@@ -395,7 +395,7 @@ export const useFetchedTypebot = ({
}, },
Error Error
>(`/api/typebots/${typebotId}`, fetcher, { >(`/api/typebots/${typebotId}`, fetcher, {
dedupingInterval: isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? undefined : 0, dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
}) })
if (error) onError(error) if (error) onError(error)
return { return {

View File

@@ -6,11 +6,6 @@ const moduleExports = {
outputStandalone: true, outputStandalone: true,
}, },
optimizeFonts: false, optimizeFonts: false,
publicRuntimeConfig: {
NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
NEXT_PUBLIC_GIPHY_API_KEY: process.env.NEXT_PUBLIC_GIPHY_API_KEY,
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
},
} }
const sentryWebpackPluginOptions = { const sentryWebpackPluginOptions = {

View File

@@ -4,7 +4,7 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
"dx": "yarn dev", "dx": "yarn dev",
"dev": "next dev -p 3000", "dev": "ENVSH_ENV=.env.local bash ../../env.sh next dev -p 3000",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",

View File

@@ -22,6 +22,8 @@ class MyDocument extends Document {
rel="stylesheet" rel="stylesheet"
/> />
<meta name="google" content="notranslate" /> <meta name="google" content="notranslate" />
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/__env.js" />
</Head> </Head>
<body> <body>
<Main /> <Main />

View File

@@ -11,7 +11,7 @@ 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' import { env, isNotEmpty } from 'utils'
const providers: Provider[] = [] const providers: Provider[] = []
@@ -23,10 +23,7 @@ if (isNotEmpty(process.env.GITHUB_CLIENT_ID))
}) })
) )
if ( if (isNotEmpty(env('SMTP_FROM')) && process.env.SMTP_AUTH_DISABLED !== 'true')
isNotEmpty(process.env.NEXT_PUBLIC_SMTP_FROM) &&
process.env.SMTP_AUTH_DISABLED !== 'true'
)
providers.push( providers.push(
EmailProvider({ EmailProvider({
server: { server: {
@@ -38,7 +35,7 @@ if (
}, },
ignoreTLS: process.env.SMTP_IGNORE_TLS === 'true', ignoreTLS: process.env.SMTP_IGNORE_TLS === 'true',
}, },
from: process.env.NEXT_PUBLIC_SMTP_FROM, from: env('SMTP_FROM'),
}) })
) )

View File

@@ -8,8 +8,8 @@ import { sendEmailNotification } from 'services/api/emails'
import { getAuthenticatedUser } from 'services/api/utils' import { getAuthenticatedUser } from 'services/api/utils'
import { import {
badRequest, badRequest,
env,
forbidden, forbidden,
isEmpty,
methodNotAllowed, methodNotAllowed,
notAuthenticated, notAuthenticated,
} from 'utils' } from 'utils'
@@ -66,7 +66,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 (isEmpty(process.env.NEXT_PUBLIC_E2E_TEST)) if (env('E2E_TEST') !== 'enabled')
await sendEmailNotification({ await sendEmailNotification({
to: email, to: email,
subject: "You've been invited to collaborate 🤝", subject: "You've been invited to collaborate 🤝",

View File

@@ -8,6 +8,7 @@ import path from 'path'
import { import {
createResults, createResults,
createTypebots, createTypebots,
freeWorkspaceId,
importTypebotInDatabase, importTypebotInDatabase,
parseDefaultGroupWithBlock, parseDefaultGroupWithBlock,
} from '../services/database' } from '../services/database'
@@ -123,7 +124,7 @@ test.describe('Results page', () => {
test("Incomplete results shouldn't be displayed", async ({ page }) => { test("Incomplete results shouldn't be displayed", async ({ page }) => {
await prisma.typebot.update({ await prisma.typebot.update({
where: { id: typebotId }, where: { id: typebotId },
data: { workspaceId: 'free' }, data: { workspaceId: freeWorkspaceId },
}) })
await page.goto(`/typebots/${typebotId}/results`) await page.goto(`/typebots/${typebotId}/results`)
await page.click('text=Unlock') await page.click('text=Unlock')

View File

@@ -2,7 +2,7 @@ import test, { expect } from '@playwright/test'
import cuid from 'cuid' import cuid from 'cuid'
import { defaultTextInputOptions } from 'models' import { defaultTextInputOptions } from 'models'
import path from 'path' import path from 'path'
import { importTypebotInDatabase } from '../services/database' import { freeWorkspaceId, importTypebotInDatabase } from '../services/database'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
test.describe.parallel('Settings page', () => { test.describe.parallel('Settings page', () => {
@@ -127,7 +127,7 @@ test.describe.parallel('Settings page', () => {
path.join(__dirname, '../fixtures/typebots/settings.json'), path.join(__dirname, '../fixtures/typebots/settings.json'),
{ {
id: typebotId, id: typebotId,
workspaceId: 'free', workspaceId: freeWorkspaceId,
} }
) )
await page.goto(`/typebots/${typebotId}/settings`) await page.goto(`/typebots/${typebotId}/settings`)

View File

@@ -1,7 +1,7 @@
import { CollaborationType, Plan, Prisma, User, WorkspaceRole } from 'db' import { CollaborationType, Plan, Prisma, User, WorkspaceRole } from 'db'
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { NextApiResponse } from 'next' import { NextApiResponse } from 'next'
import { forbidden, isNotEmpty } from 'utils' import { env, forbidden, isNotEmpty } from 'utils'
const parseWhereFilter = ( const parseWhereFilter = (
typebotIds: string[] | string, typebotIds: string[] | string,
@@ -22,7 +22,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) ||
isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST) isNotEmpty(env('E2E_TEST'))
? undefined ? undefined
: { : {
members: { members: {

View File

@@ -1,4 +1,5 @@
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import { env } from 'utils'
export const sendEmailNotification = ({ export const sendEmailNotification = ({
to, to,
@@ -19,7 +20,7 @@ export const sendEmailNotification = ({
}) })
return transporter.sendMail({ return transporter.sendMail({
from: process.env.NEXT_PUBLIC_SMTP_FROM, from: env('SMTP_FORM'),
to, to,
subject, subject,
html: content, html: content,

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 { isNotEmpty, sendRequest } from 'utils' import { env, sendRequest } from 'utils'
export const useFolders = ({ export const useFolders = ({
parentId, parentId,
@@ -18,9 +18,7 @@ export const useFolders = ({
workspaceId ? `/api/folders?${params}` : null, workspaceId ? `/api/folders?${params}` : null,
fetcher, fetcher,
{ {
dedupingInterval: isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST) dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
? 0
: undefined,
} }
) )
if (error) onError(error) if (error) onError(error)

View File

@@ -1,4 +1,4 @@
import { sendRequest } from 'utils' import { env, sendRequest } from 'utils'
import { stringify } from 'qs' import { stringify } from 'qs'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from './utils' import { fetcher } from './utils'
@@ -78,7 +78,9 @@ export const executeWebhook = (
{ blockId }: { blockId: string } { blockId }: { blockId: string }
) => ) =>
sendRequest<WebhookResponse>({ sendRequest<WebhookResponse>({
url: `${process.env.NEXT_PUBLIC_VIEWER_URL}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook`, url: `${env(
'VIEWER_URL'
)}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook`,
method: 'POST', method: 'POST',
body: { body: {
variables, variables,

View File

@@ -1,7 +1,6 @@
import { Plan, User } from 'db' import { Plan, User } from 'db'
import { loadStripe } from '@stripe/stripe-js/pure' import { loadStripe } from '@stripe/stripe-js/pure'
import { isDefined, isEmpty, sendRequest } from 'utils' import { env, isDefined, isEmpty, sendRequest } from 'utils'
import getConfig from 'next/config'
type Props = { type Props = {
user: User user: User
@@ -40,10 +39,7 @@ const redirectToCheckout = async ({
plan, plan,
workspaceId, workspaceId,
}: Omit<Props, 'customerId'>) => { }: Omit<Props, 'customerId'>) => {
const { if (isEmpty(env('STRIPE_PUBLIC_KEY')))
publicRuntimeConfig: { NEXT_PUBLIC_STRIPE_PUBLIC_KEY },
} = getConfig()
if (isEmpty(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 { data, error } = await sendRequest<{ sessionId: string }>({ const { data, error } = await sendRequest<{ sessionId: string }>({
method: 'POST', method: 'POST',
@@ -57,7 +53,7 @@ const redirectToCheckout = async ({
}, },
}) })
if (error || !data) return if (error || !data) return
const stripe = await loadStripe(NEXT_PUBLIC_STRIPE_PUBLIC_KEY) const stripe = await loadStripe(env('STRIPE_PUBLIC_KEY') as string)
await stripe?.redirectToCheckout({ await stripe?.redirectToCheckout({
sessionId: data?.sessionId, sessionId: data?.sessionId,
}) })

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 { isNotEmpty, sendRequest } from 'utils' import { env, sendRequest } from 'utils'
export const useInvitations = ({ export const useInvitations = ({
typebotId, typebotId,
@@ -14,9 +14,7 @@ export const useInvitations = ({
typebotId ? `/api/typebots/${typebotId}/invitations` : null, typebotId ? `/api/typebots/${typebotId}/invitations` : null,
fetcher, fetcher,
{ {
dedupingInterval: isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST) dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
? 0
: undefined,
} }
) )
if (error) onError(error) if (error) onError(error)

View File

@@ -7,7 +7,7 @@ import {
import useSWRInfinite from 'swr/infinite' import useSWRInfinite from 'swr/infinite'
import { stringify } from 'qs' import { stringify } from 'qs'
import { Answer } from 'db' import { Answer } from 'db'
import { isDefined, isEmpty, sendRequest } from 'utils' import { env, isDefined, sendRequest } from 'utils'
import { fetcher } from 'services/utils' import { fetcher } from 'services/utils'
import { HStack, Text, Wrap, WrapItem } from '@chakra-ui/react' import { HStack, Text, Wrap, WrapItem } from '@chakra-ui/react'
import { CodeIcon, CalendarIcon, FileIcon } from 'assets/icons' import { CodeIcon, CalendarIcon, FileIcon } from 'assets/icons'
@@ -54,9 +54,7 @@ export const useResults = ({
fetcher, fetcher,
{ {
revalidateAll: true, revalidateAll: true,
dedupingInterval: isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
? undefined
: 0,
} }
) )

View File

@@ -46,13 +46,13 @@ import useSWR from 'swr'
import { fetcher, toKebabCase } from '../utils' import { fetcher, toKebabCase } from '../utils'
import { import {
isBubbleBlockType, isBubbleBlockType,
isNotEmpty,
isWebhookBlock, isWebhookBlock,
omit, omit,
blockHasItems, blockHasItems,
blockTypeHasItems, blockTypeHasItems,
blockTypeHasOption, blockTypeHasOption,
blockTypeHasWebhook, blockTypeHasWebhook,
env,
} from 'utils' } from 'utils'
import { dequal } from 'dequal' import { dequal } from 'dequal'
import { stringify } from 'qs' import { stringify } from 'qs'
@@ -83,9 +83,7 @@ export const useTypebots = ({
{ typebots: TypebotInDashboard[] }, { typebots: TypebotInDashboard[] },
Error Error
>(workspaceId ? `/api/typebots?${params}` : null, fetcher, { >(workspaceId ? `/api/typebots?${params}` : null, fetcher, {
dedupingInterval: isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST) dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
? 0
: undefined,
}) })
if (error) onError(error) if (error) onError(error)
return { return {

View File

@@ -1,7 +1,7 @@
import { ApiToken } from 'db' import { ApiToken } from 'db'
import { fetcher } from 'services/utils' import { fetcher } from 'services/utils'
import useSWR, { KeyedMutator } from 'swr' import useSWR, { KeyedMutator } from 'swr'
import { isNotEmpty, sendRequest } from 'utils' import { env, sendRequest } from 'utils'
export type ApiTokenFromServer = { id: string; name: string; createdAt: string } export type ApiTokenFromServer = { id: string; name: string; createdAt: string }
@@ -25,9 +25,7 @@ export const useApiTokens = ({
userId ? `/api/users/${userId}/api-tokens` : null, userId ? `/api/users/${userId}/api-tokens` : null,
fetcher, fetcher,
{ {
dedupingInterval: isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST) dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
? 0
: undefined,
} }
) )
if (error) onError(error) if (error) onError(error)

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 { isEmpty, sendRequest } from 'utils' import { env, 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: isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? undefined : 0, dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
}) })
return { return {
members: data?.members, members: data?.members,

View File

@@ -1,2 +1,2 @@
# Don't edit this file # Don't edit this file
NEXT_PUBLIC_VIEWER_URL=DOCKER_NEXT_PUBLIC_VIEWER_URL NEXT_PUBLIC_VIEWER_URL=

View File

@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { isEmpty } from 'utils' import { env, isEmpty } from 'utils'
export const ErrorPage = ({ error }: { error: Error }) => { export const ErrorPage = ({ error }: { error: Error }) => {
return ( return (
@@ -12,7 +12,7 @@ export const ErrorPage = ({ error }: { error: Error }) => {
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
{isEmpty(process.env.NEXT_PUBLIC_VIEWER_URL) ? ( {isEmpty(env('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

@@ -4,7 +4,7 @@
"version": "0.1.0", "version": "0.1.0",
"scripts": { "scripts": {
"dx": "yarn dev", "dx": "yarn dev",
"dev": "next dev -p 3001", "dev": "ENVSH_ENV=.env.local bash ../../env.sh next dev -p 3001",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",

View File

@@ -3,7 +3,7 @@ import { NotFoundPage } from 'layouts/NotFoundPage'
import { PublicTypebot } from 'models' import { PublicTypebot } from 'models'
import { GetServerSideProps, GetServerSidePropsContext } from 'next' import { GetServerSideProps, GetServerSidePropsContext } from 'next'
import sanitizeHtml from 'sanitize-html' import sanitizeHtml from 'sanitize-html'
import { isDefined, isNotDefined, omit } from 'utils' import { env, isDefined, isNotDefined, omit } from 'utils'
import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage' import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage'
import prisma from '../libs/prisma' import prisma from '../libs/prisma'
@@ -16,9 +16,9 @@ export const getServerSideProps: GetServerSideProps = async (
const { host, forwardedHost } = getHost(context.req) const { host, forwardedHost } = getHost(context.req)
try { try {
if (!host) return { props: {} } if (!host) return { props: {} }
const viewerUrls = (process.env.NEXT_PUBLIC_VIEWER_URL ?? '').split(',') const viewerUrls = (env('VIEWER_URL') ?? '').split(',')
const isMatchingViewerUrl = const isMatchingViewerUrl =
process.env.NEXT_PUBLIC_E2E_TEST === 'enabled' env('E2E_TEST') === 'enabled'
? true ? true
: viewerUrls.some( : viewerUrls.some(
(url) => (url) =>

View File

@@ -0,0 +1,31 @@
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html>
<Head>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/__env.js" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

View File

@@ -1,5 +1,5 @@
import { CollaborationType, Prisma, User, WorkspaceRole } from 'db' import { CollaborationType, Prisma, User, WorkspaceRole } from 'db'
import { isNotEmpty } from 'utils' import { env } from 'utils'
const parseWhereFilter = ( const parseWhereFilter = (
typebotIds: string[] | string, typebotIds: string[] | string,
@@ -20,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) ||
isNotEmpty(process.env.NEXT_PUBLIC_E2E_TEST) env('E2E_TEST') === 'enabled'
? undefined ? undefined
: { : {
members: { members: {

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
./entrypoint.sh ./env.sh
./node_modules/.bin/prisma generate; ./node_modules/.bin/prisma generate;

View File

@@ -4,7 +4,7 @@ services:
image: postgres:13 image: postgres:13
restart: always restart: always
volumes: volumes:
- db_data:/var/lib/postgresql/data - build_db_data:/var/lib/postgresql/data
environment: environment:
- POSTGRES_DB=typebot - POSTGRES_DB=typebot
- POSTGRES_PASSWORD=typebot - POSTGRES_PASSWORD=typebot
@@ -28,7 +28,6 @@ services:
- ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6 - ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6
- ADMIN_EMAIL=me@email.com - ADMIN_EMAIL=me@email.com
- NEXTAUTH_URL_INTERNAL=http://host.docker.internal:8080 - NEXTAUTH_URL_INTERNAL=http://host.docker.internal:8080
- NEXT_PUBLIC_GOOGLE_CLIENT_ID=fnejwkn
typebot-viewer: typebot-viewer:
build: build:
context: . context: .
@@ -43,4 +42,4 @@ services:
- 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
volumes: volumes:
db_data: build_db_data:

161
env.sh Normal file
View File

@@ -0,0 +1,161 @@
#!/bin/bash
# env.sh - Dead-simple .env file reader and generator
# Tunghsiao Liu (t@sparanoid.com)
#
# Inspired by:
# - https://github.com/andrewmclagan/react-env
# - https://www.freecodecamp.org/news/7f9d42a91d70/
# - https://github.com/kunokdev/cra-runtime-environment-variables
#
# Features:
# - Designed to be used for Next.js app inside a Docker container (General
# React app should also work)
# - Read env file and generate __env.js file for runtime client use
# - Merge current environment variables passing to it (Useful for Docker images)
# - No dependencies (More like a lite version of react-env). This is important
# to keep our container image as small as possible.
#
# Usage:
# - General usage:
# $ ./env.sh
#
# - Replacing varaible:
# $ NEXT_PUBLIC_API_BASE=xxx ./env.sh
#
# - Enviroment variable not in whitelist will be discarded:
# $ BAD_ENV=zzz ./env.sh
#
# - Change script options:
# $ ENVSH_ENV="./.env.staging" ENVSH_OUTPUT="./public/config.js" ./env.sh
#
# - Use it inside Dockerfile:
# RUN chmod +x ./env.sh
# ENTRYPOINT ["./env.sh"]
#
# Debug:
# NEXT_PUBLIC_OB_ENV=123_from_fish NEXT_BAD_ENV=zzz NEXT_PUBLIC_OB_TESTNEW=testenv NEXT_PUBLIC_CODE_UPLOAD_SIZE_LIMIT=6666 ./env.sh
echo -e "env.sh loaded"
# Config
ENVSH_ENV="${ENVSH_ENV:-"./.env.production"}"
ENVSH_PREFIX="${ENVSH_PREFIX:-"NEXT_PUBLIC_"}"
ENVSH_PREFIX_STRIP="${ENVSH_PREFIX_STRIP:-true}"
# Can be `window.__env = {` or `const ENV = {` or whatever you want
ENVSH_PREPEND="${ENVSH_PREPEND:-"window.__env = {"}"
ENVSH_APPEND="${ENVSH_APPEND:-"}"}"
ENVSH_OUTPUT="${ENVSH_OUTPUT:-"./public/__env.js"}"
# Utils
__green() {
printf '\033[1;31;32m%b\033[0m' "$1"
}
__yellow() {
printf '\033[1;31;33m%b\033[0m' "$1"
}
__red() {
printf '\033[1;31;40m%b\033[0m' "$1"
}
__info() {
printf "%s\n" "$1"
}
__debug() {
ENVSH_VERBOSE="${ENVSH_VERBOSE:-"false"}"
if [ "$ENVSH_VERBOSE" == "true" ]; then
printf "ENVSH_VERBOSE: %s\n" "$1"
fi
}
ENVSH_SED="sed"
if [[ "$OSTYPE" == "darwin"* ]]; then
echo "macOS detected, switching to gsed"
if command -v gsed >/dev/null 2>&1 ; then
__info "$(__green "Found"): $(gsed --version | head -n 1)"
else
__info "gsed not found, trying to install..."
if command -v brew >/dev/null 2>&1 ; then
__info "$(__green "Found"): $(brew --version | head -n 1)"
brew install gnu-sed
else
__info "$(__red "Homebrew not found, install it first: https://brew.sh/")"
exit 1;
fi
fi
ENVSH_SED="gsed"
fi
# Recreate config file
rm -f "$ENVSH_OUTPUT"
touch "$ENVSH_OUTPUT"
# Create an array from inline variables
matched_envs=$(env | grep ${ENVSH_PREFIX})
IFS=$'\n' read -r -d '' -a matched_envs_arr <<< "$matched_envs"
__info "Matched inline env:"
for matched_env in "${matched_envs_arr[@]}"; do
echo $matched_env
done
# Add assignment
echo "$ENVSH_PREPEND" >> "$ENVSH_OUTPUT"
# Check if file exists
[[ -f "$ENVSH_ENV" ]] || { echo "$ENVSH_ENV does not exist" ; exit 1 ;}
# Process .env for runtime client use
__info "$(__green "Reading ${ENVSH_ENV}...")"
while IFS= read -r line
do
# Check if this line is a valid environment variable and matches our prefix
if printf '%s' "$line" | grep -e "=" | grep -e "$ENVSH_PREFIX"; then
# Read and apply environment variable if exists
# NOTE: <<< here operator not working with `sh`
awk -F '=' '{print $1 ": \"" (ENVIRON[$1] ? ENVIRON[$1] : $2) "\","}' \
<<< "$line" >> "$ENVSH_OUTPUT"
fi
done < "$ENVSH_ENV"
echo "$ENVSH_APPEND" >> "$ENVSH_OUTPUT"
# Strip prefix if needed
$ENVSH_PREFIX_STRIP && $ENVSH_SED -i'' -e "s~$ENVSH_PREFIX~~g" "$ENVSH_OUTPUT"
# NOTE: This step is not necessary because variables on pages inside the
# Next.js prod server won't be changed. They're already inlined during the
# build time.
for matched_env in "${matched_envs_arr[@]}"; do
echo $matched_env
done
for i in "${!matched_envs_arr[@]}"; do
IFS='=' read -ra key_arr <<< "${matched_envs_arr[$i]}"
key=${key_arr[0]}
if [[ "${matched_envs_arr[$i]}" = *"${key}"* ]]; then
index="$i"
__info "Got index from inline env: ${index}, replacing ${key}"
find "$ENVSH_ENV" -type f -exec $ENVSH_SED -i'' \
-e "s~$key=.*~${matched_envs_arr[$index]}~g" {} \;
fi
done
# Print result
__debug "$(__green "Done! Final result in ${ENVSH_OUTPUT}:")"
__debug "`cat "$ENVSH_OUTPUT"`"
__debug "$(__green "Done! Modified ${ENVSH_ENV}:")"
__debug "`cat "$ENVSH_ENV"`"
__info "$(__green "env.sh done\n")"
# Accepting commands (for Docker)
exec "$@"

View File

@@ -11,7 +11,7 @@
"docker:up": "docker compose -f docker-compose.dev.yml up -d", "docker:up": "docker compose -f docker-compose.dev.yml up -d",
"docker:nuke": "docker compose -f docker-compose.dev.yml down --volumes --remove-orphans", "docker:nuke": "docker compose -f docker-compose.dev.yml down --volumes --remove-orphans",
"dev:prepare": "turbo run build --scope=bot-engine --no-deps --include-dependencies && turbo run build --scope=typebot-js --no-deps", "dev:prepare": "turbo run build --scope=bot-engine --no-deps --include-dependencies && turbo run build --scope=typebot-js --no-deps",
"dev": "yarn docker:up && yarn dev:prepare && turbo run dx --parallel", "dev": "yarn docker:up && yarn dev:prepare && NEXT_PUBLIC_E2E_TEST=false turbo run dx --parallel",
"dev:mocking": "yarn docker:up && NEXT_PUBLIC_E2E_TEST=enabled turbo run dx --parallel", "dev:mocking": "yarn docker:up && NEXT_PUBLIC_E2E_TEST=enabled turbo run dx --parallel",
"build": "yarn docker:up && turbo run build", "build": "yarn docker:up && turbo run build",
"test:builder": "cd apps/builder && yarn test", "test:builder": "cd apps/builder && yarn test",

View File

@@ -21,7 +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' import { env, isEmpty } from 'utils'
export type TypebotViewerProps = { export type TypebotViewerProps = {
typebot: PublicTypebot typebot: PublicTypebot
@@ -41,7 +41,7 @@ export type TypebotViewerProps = {
export const TypebotViewer = ({ export const TypebotViewer = ({
typebot, typebot,
apiHost = process.env.NEXT_PUBLIC_VIEWER_URL?.split(',')[0], apiHost = env('VIEWER_URL')?.split(',')[0],
isPreview = false, isPreview = false,
isLoading = false, isLoading = false,
style, style,

View File

@@ -243,3 +243,19 @@ export const uploadFiles = async ({
} }
return urls return urls
} }
declare const window: any
const isBrowser = () => {
return Boolean(typeof window !== 'undefined' && window.__env)
}
export const env = (key = ''): string | undefined => {
if (isBrowser() && window.__env) {
return window.__env[key] === "''" ? undefined : window.__env[key]
}
return process.env['NEXT_PUBLIC_' + key] === "''"
? undefined
: (process.env['NEXT_PUBLIC_' + key] as string)
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
./entrypoint.sh ./env.sh
./node_modules/.bin/prisma generate; ./node_modules/.bin/prisma generate;