2
0

🧑‍💻 Improve env variables type safety and management (#718)

Closes #679
This commit is contained in:
Baptiste Arnaud
2023-08-28 09:13:53 +02:00
committed by GitHub
parent a23a8c4456
commit 786e5cb582
148 changed files with 1550 additions and 1293 deletions

View File

@@ -3,14 +3,14 @@ import { GiphyFetch } from '@giphy/js-fetch-api'
import { Grid } from '@giphy/react-components'
import { GiphyLogo } from '../logos/GiphyLogo'
import React, { useState } from 'react'
import { env, isEmpty } from '@typebot.io/lib'
import { TextInput } from '../inputs'
import { env } from '@typebot.io/env'
type GiphySearchFormProps = {
onSubmit: (url: string) => void
}
const giphyFetch = new GiphyFetch(env('GIPHY_API_KEY') as string)
const giphyFetch = new GiphyFetch(env.NEXT_PUBLIC_GIPHY_API_KEY ?? '')
export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => {
const [inputValue, setInputValue] = useState('')
@@ -21,7 +21,7 @@ export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => {
const fetchGifsTrending = (offset: number) =>
giphyFetch.trending({ offset, limit: 10 })
return isEmpty(env('GIPHY_API_KEY')) ? (
return !env.NEXT_PUBLIC_GIPHY_API_KEY ? (
<Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text>
) : (
<Stack spacing={4} pt="2">

View File

@@ -14,16 +14,17 @@ import {
Text,
useColorModeValue,
} from '@chakra-ui/react'
import { env, isDefined, isEmpty } from '@typebot.io/lib'
import { isDefined } from '@typebot.io/lib'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createApi } from 'unsplash-js'
import { Basic as UnsplashImage } from 'unsplash-js/dist/methods/photos/types'
import { TextInput } from '../inputs'
import { UnsplashLogo } from '../logos/UnsplashLogo'
import { TextLink } from '../TextLink'
import { env } from '@typebot.io/env'
const api = createApi({
accessKey: env('UNSPLASH_ACCESS_KEY') ?? '',
accessKey: env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY ?? '',
})
type Props = {
@@ -124,7 +125,7 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
searchRandomImages()
}, [])
if (isEmpty(env('UNSPLASH_ACCESS_KEY')))
if (!env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY)
return (
<Text>NEXT_PUBLIC_UNSPLASH_ACCESS_KEY is missing in environment</Text>
)
@@ -143,9 +144,7 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
/>
<Link
isExternal
href={`https://unsplash.com/?utm_source=${env(
'UNSPLASH_APP_NAME'
)}&utm_medium=referral`}
href={`https://unsplash.com/?utm_source=${env.NEXT_PUBLIC_UNSPLASH_APP_NAME}&utm_medium=referral`}
>
<UnsplashLogo width="80px" fill={unsplashLogoFillColor} />
</Link>
@@ -224,9 +223,7 @@ const UnsplashImage = ({ image, onClick }: UnsplashImageProps) => {
<TextLink
fontSize="xs"
isExternal
href={`https://unsplash.com/@${user.username}?utm_source=${env(
'UNSPLASH_APP_NAME'
)}&utm_medium=referral`}
href={`https://unsplash.com/@${user.username}?utm_source=${env.NEXT_PUBLIC_UNSPLASH_APP_NAME}&utm_medium=referral`}
noOfLines={1}
color="white"
>

View File

@@ -13,7 +13,7 @@ import {
} from '@chakra-ui/react'
import { useState, useRef, useEffect, ReactNode } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env, isDefined } from '@typebot.io/lib'
import { isDefined } from '@typebot.io/lib'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
import { VariablesButton } from '@/features/variables/components/VariablesButton'
@@ -21,6 +21,7 @@ import { Variable } from '@typebot.io/schemas'
import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput'
import { focusInput } from '@/helpers/focusInput'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
import { env } from '@typebot.io/env'
type Props = {
items: string[]
@@ -57,7 +58,7 @@ export const AutocompleteInput = ({
const onChange = useDebouncedCallback(
_onChange,
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
)
useEffect(() => {

View File

@@ -9,7 +9,7 @@ import { useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { VariablesButton } from '@/features/variables/components/VariablesButton'
import { Variable } from '@typebot.io/schemas'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror'
import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'
import { githubLight } from '@uiw/codemirror-theme-github'
@@ -53,7 +53,7 @@ export const CodeEditor = ({
_setValue(value)
onChange && onChange(value)
},
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
)
const handleVariableSelected = (variable?: Pick<Variable, 'id' | 'name'>) => {

View File

@@ -14,7 +14,7 @@ import {
import { Variable, VariableString } from '@typebot.io/schemas'
import { useEffect, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
type Value<HasVariable> = HasVariable extends true | undefined
@@ -47,7 +47,7 @@ export const NumberInput = <HasVariable extends boolean>({
const onValueChangeDebounced = useDebouncedCallback(
onValueChange,
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
)
useEffect(

View File

@@ -19,7 +19,7 @@ import React, {
useState,
} from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
export type TextInputProps = {
@@ -69,7 +69,7 @@ export const TextInput = forwardRef(function TextInput(
const onChange = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function
_onChange ?? (() => {}),
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
)
useEffect(() => {

View File

@@ -11,7 +11,7 @@ import {
import { Variable } from '@typebot.io/schemas'
import React, { useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
type Props = {
@@ -46,7 +46,7 @@ export const Textarea = ({
)
const onChange = useDebouncedCallback(
_onChange,
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
)
useEffect(() => {

View File

@@ -1,12 +1,13 @@
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { createContext, ReactNode, useEffect, useState } from 'react'
import { env, isDefined, isNotDefined } from '@typebot.io/lib'
import { isDefined, isNotDefined } from '@typebot.io/lib'
import { User } from '@typebot.io/prisma'
import { setUser as setSentryUser } from '@sentry/nextjs'
import { useToast } from '@/hooks/useToast'
import { updateUserQuery } from './queries/updateUserQuery'
import { useDebouncedCallback } from 'use-debounce'
import { env } from '@typebot.io/env'
export const userContext = createContext<{
user?: User
@@ -66,7 +67,7 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
if (error) showToast({ title: error.name, description: error.message })
await refreshUser()
},
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
)
useEffect(() => {

View File

@@ -1,5 +1,6 @@
import { getTestAsset } from '@/test/utils/playwright'
import test, { expect } from '@playwright/test'
import { env } from '@typebot.io/env'
import { userId } from '@typebot.io/lib/playwright/databaseSetup'
test.describe.configure({ mode: 'parallel' })
@@ -15,9 +16,9 @@ test('should display user info properly', async ({ page }) => {
await expect(page.locator('img >> nth=1')).toHaveAttribute(
'src',
new RegExp(
`${process.env.S3_ENDPOINT}${
process.env.S3_PORT ? `:${process.env.S3_PORT}` : ''
}/${process.env.S3_BUCKET}/public/users/${userId}/avatar`,
`${env.S3_ENDPOINT}${env.S3_PORT ? `:${env.S3_PORT}` : ''}/${
env.S3_BUCKET
}/public/users/${userId}/avatar`,
'gm'
)
)

View File

@@ -1,6 +1,6 @@
import { fetcher } from '@/helpers/fetcher'
import useSWR from 'swr'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
import { ApiTokenFromServer } from '../types'
type ServerResponse = {
@@ -18,7 +18,7 @@ export const useApiTokens = ({
userId ? `/api/users/${userId}/api-tokens` : null,
fetcher,
{
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
dedupingInterval: env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
}
)
if (error) onError(error)

View File

@@ -14,6 +14,7 @@ import { convertInvitationsToCollaborations } from '@/features/auth/helpers/conv
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
import { joinWorkspaces } from '@/features/auth/helpers/joinWorkspaces'
import { parseWorkspaceDefaultPlan } from '@/features/workspace/helpers/parseWorkspaceDefaultPlan'
import { env } from '@typebot.io/env'
export function customAdapter(p: PrismaClient): Adapter {
return {
@@ -26,8 +27,8 @@ export function customAdapter(p: PrismaClient): Adapter {
user.email
)
if (
process.env.DISABLE_SIGNUP === 'true' &&
process.env.ADMIN_EMAIL !== user.email &&
env.DISABLE_SIGNUP &&
env.ADMIN_EMAIL !== user.email &&
invitations.length === 0 &&
workspaceInvitations.length === 0
)

View File

@@ -13,8 +13,8 @@ import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import confetti from 'canvas-confetti'
import { useUser } from '@/features/account/hooks/useUser'
import { env, isEmpty } from '@typebot.io/lib'
import { useI18n } from '@/locales'
import { env } from '@typebot.io/env'
const totalSteps = 5
@@ -37,7 +37,7 @@ export const OnboardingPage = () => {
useEffect(() => {
if (!user?.createdAt) return
if (isNewUser === false || isEmpty(env('ONBOARDING_TYPEBOT_ID')))
if (isNewUser === false || !env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID)
replace('/typebots')
}, [isNewUser, replace, user?.createdAt])
@@ -90,7 +90,7 @@ export const OnboardingPage = () => {
<Dots currentStep={currentStep} pos="fixed" top="9" />
<Flex w="full" maxW="800px" h="full" maxH="70vh" rounded="lg">
<Standard
typebot={env('ONBOARDING_TYPEBOT_ID')}
typebot={env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID}
style={{ borderRadius: '1rem' }}
prefilledVariables={{ Name: user?.name, Email: user?.email }}
onEnd={() => {

View File

@@ -5,7 +5,7 @@ import { User } from '@typebot.io/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getServerSession } from 'next-auth'
import { mockedUser } from '../mockedUser'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
export const getAuthenticatedUser = async (
req: NextApiRequest,
@@ -13,12 +13,11 @@ export const getAuthenticatedUser = async (
): Promise<User | undefined> => {
const bearerToken = extractBearerToken(req)
if (bearerToken) return authenticateByToken(bearerToken)
const user =
env('E2E_TEST') === 'true'
? mockedUser
: ((await getServerSession(req, res, authOptions))?.user as
| User
| undefined)
const user = env.NEXT_PUBLIC_E2E_TEST
? mockedUser
: ((await getServerSession(req, res, authOptions))?.user as
| User
| undefined)
if (!user || !('id' in user)) return
setUser({ id: user.id })
return user

View File

@@ -6,6 +6,7 @@ import Stripe from 'stripe'
import { z } from 'zod'
import { parseSubscriptionItems } from '../helpers/parseSubscriptionItems'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
export const createCheckoutSession = authenticatedProcedure
.meta({
@@ -57,7 +58,7 @@ export const createCheckoutSession = authenticatedProcedure
},
ctx: { user },
}) => {
if (!process.env.STRIPE_SECRET_KEY)
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
@@ -82,14 +83,13 @@ export const createCheckoutSession = authenticatedProcedure
code: 'NOT_FOUND',
message: 'Workspace not found',
})
if (workspace.stripeId)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Customer already exists, use updateSubscription endpoint.',
})
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})

View File

@@ -5,6 +5,7 @@ import { Plan } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
export const createCustomCheckoutSession = authenticatedProcedure
.meta({
@@ -31,7 +32,7 @@ export const createCustomCheckoutSession = authenticatedProcedure
)
.mutation(
async ({ input: { email, workspaceId, returnUrl }, ctx: { user } }) => {
if (!process.env.STRIPE_SECRET_KEY)
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
@@ -61,7 +62,7 @@ export const createCustomCheckoutSession = authenticatedProcedure
code: 'NOT_FOUND',
message: 'Custom plan not found',
})
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})

View File

@@ -4,6 +4,7 @@ import { TRPCError } from '@trpc/server'
import Stripe from 'stripe'
import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
export const getBillingPortalUrl = authenticatedProcedure
.meta({
@@ -26,7 +27,7 @@ export const getBillingPortalUrl = authenticatedProcedure
})
)
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
if (!process.env.STRIPE_SECRET_KEY)
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'STRIPE_SECRET_KEY var is missing',
@@ -50,12 +51,12 @@ export const getBillingPortalUrl = authenticatedProcedure
code: 'NOT_FOUND',
message: 'Workspace not found',
})
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const portalSession = await stripe.billingPortal.sessions.create({
customer: workspace.stripeId,
return_url: `${process.env.NEXTAUTH_URL}/typebots`,
return_url: `${env.NEXTAUTH_URL}/typebots`,
})
return {
billingPortalUrl: portalSession.url,

View File

@@ -4,8 +4,9 @@ import { TRPCError } from '@trpc/server'
import Stripe from 'stripe'
import { z } from 'zod'
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
import { priceIds } from '@typebot.io/lib/pricing'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { priceIds } from '@typebot.io/lib/api/pricing'
import { env } from '@typebot.io/env'
export const getSubscription = authenticatedProcedure
.meta({
@@ -28,7 +29,7 @@ export const getSubscription = authenticatedProcedure
})
)
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
if (!process.env.STRIPE_SECRET_KEY)
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
@@ -55,7 +56,7 @@ export const getSubscription = authenticatedProcedure
return {
subscription: null,
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const subscriptions = await stripe.subscriptions.list({

View File

@@ -6,6 +6,7 @@ import { isDefined } from '@typebot.io/lib'
import { z } from 'zod'
import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
export const listInvoices = authenticatedProcedure
.meta({
@@ -28,7 +29,7 @@ export const listInvoices = authenticatedProcedure
})
)
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
if (!process.env.STRIPE_SECRET_KEY)
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'STRIPE_SECRET_KEY var is missing',
@@ -52,7 +53,7 @@ export const listInvoices = authenticatedProcedure
code: 'NOT_FOUND',
message: 'Workspace not found',
})
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const invoices = await stripe.invoices.list({

View File

@@ -7,15 +7,13 @@ import { workspaceSchema } from '@typebot.io/schemas'
import Stripe from 'stripe'
import { isDefined } from '@typebot.io/lib'
import { z } from 'zod'
import {
getChatsLimit,
getStorageLimit,
priceIds,
} from '@typebot.io/lib/pricing'
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
import { chatPriceIds, storagePriceIds } from './getSubscription'
import { createCheckoutSessionUrl } from './createCheckoutSession'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { getUsage } from '@typebot.io/lib/api/getUsage'
import { env } from '@typebot.io/env'
import { priceIds } from '@typebot.io/lib/api/pricing'
export const updateSubscription = authenticatedProcedure
.meta({
@@ -57,7 +55,7 @@ export const updateSubscription = authenticatedProcedure
},
ctx: { user },
}) => {
if (!process.env.STRIPE_SECRET_KEY)
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
@@ -85,7 +83,7 @@ export const updateSubscription = authenticatedProcedure
code: 'NOT_FOUND',
message: 'Workspace not found',
})
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const { data } = await stripe.subscriptions.list({
@@ -95,10 +93,9 @@ export const updateSubscription = authenticatedProcedure
})
const subscription = data[0] as Stripe.Subscription | undefined
const currentPlanItemId = subscription?.items.data.find((item) =>
[
process.env.STRIPE_STARTER_PRODUCT_ID,
process.env.STRIPE_PRO_PRODUCT_ID,
].includes(item.price.product.toString())
[env.STRIPE_STARTER_PRODUCT_ID, env.STRIPE_PRO_PRODUCT_ID].includes(
item.price.product.toString()
)
)?.id
const currentAdditionalChatsItemId = subscription?.items.data.find(
(item) => chatPriceIds.includes(item.price.id)

View File

@@ -12,6 +12,7 @@ import {
deleteWorkspaces,
injectFakeResults,
} from '@typebot.io/lib/playwright/databaseActions'
import { env } from '@typebot.io/env'
const usageWorkspaceId = createId()
const usageTypebotId = createId()
@@ -147,7 +148,7 @@ test('plan changes should work', async ({ page }) => {
planChangeWorkspaceId,
[
{
price: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID,
price: env.STRIPE_STARTER_MONTHLY_PRICE_ID,
quantity: 1,
},
],

View File

@@ -1,8 +1,5 @@
import {
getChatsLimit,
getStorageLimit,
priceIds,
} from '@typebot.io/lib/pricing'
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
import { priceIds } from '@typebot.io/lib/api/pricing'
export const parseSubscriptionItems = (
plan: 'STARTER' | 'PRO',

View File

@@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseH
import { defaultPaymentInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { stripePaymentForm } from '@/test/utils/selectorUtils'
import { env } from '@typebot.io/env'
test.describe('Payment input block', () => {
test('Can configure Stripe account', async ({ page }) => {
@@ -23,21 +24,15 @@ test.describe('Payment input block', () => {
await page.getByRole('button', { name: 'Select an account' }).click()
await page.click('text=Connect new')
await page.fill('[placeholder="Typebot"]', 'My Stripe Account')
await page.fill(
'[placeholder="sk_test_..."]',
process.env.STRIPE_SECRET_KEY ?? ''
)
await page.fill(
'[placeholder="sk_live_..."]',
process.env.STRIPE_SECRET_KEY ?? ''
)
await page.fill('[placeholder="sk_test_..."]', env.STRIPE_SECRET_KEY ?? '')
await page.fill('[placeholder="sk_live_..."]', env.STRIPE_SECRET_KEY ?? '')
await page.fill(
'[placeholder="pk_test_..."]',
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? ''
env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? ''
)
await page.fill(
'[placeholder="pk_live_..."]',
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? ''
env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY ?? ''
)
await expect(page.locator('button >> text="Connect"')).toBeEnabled()
await page.click('button >> text="Connect"')

View File

@@ -10,7 +10,7 @@ import {
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { SendEmailOptions, Variable } from '@typebot.io/schemas'
import React from 'react'
import { env, isNotEmpty } from '@typebot.io/lib'
import { isNotEmpty } from '@typebot.io/lib'
import { SmtpConfigModal } from './SmtpConfigModal'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
@@ -18,6 +18,7 @@ import { CredentialsDropdown } from '@/features/credentials/components/Credentia
import { TextInput, Textarea } from '@/components/inputs'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { env } from '@typebot.io/env'
type Props = {
options: SendEmailOptions
@@ -117,9 +118,9 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
currentCredentialsId={options.credentialsId}
onCredentialsSelect={handleCredentialsSelect}
onCreateNewClick={onOpen}
defaultCredentialLabel={env('SMTP_FROM')
?.match(/<(.*)>/)
?.pop()}
defaultCredentialLabel={env.NEXT_PUBLIC_SMTP_FROM?.match(
/<(.*)>/
)?.pop()}
/>
)}
</Stack>

View File

@@ -2,17 +2,18 @@ import test, { expect } from '@playwright/test'
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { env } from '@typebot.io/env'
const typebotId = createId()
test.describe('Send email block', () => {
test('its configuration should work', async ({ page }) => {
if (
!process.env.SMTP_USERNAME ||
!process.env.SMTP_PORT ||
!process.env.SMTP_HOST ||
!process.env.SMTP_PASSWORD ||
!process.env.NEXT_PUBLIC_SMTP_FROM
!env.SMTP_USERNAME ||
!env.SMTP_PORT ||
!env.SMTP_HOST ||
!env.SMTP_PASSWORD ||
!env.NEXT_PUBLIC_SMTP_FROM
)
throw new Error('SMTP_ env vars are missing')
await importTypebotInDatabase(
@@ -30,21 +31,18 @@ test.describe('Send email block', () => {
await expect(createButton).toBeDisabled()
await page.fill(
'[placeholder="notifications@provider.com"]',
process.env.SMTP_USERNAME
env.SMTP_USERNAME
)
await page.fill('[placeholder="John Smith"]', 'John Smith')
await page.fill('[placeholder="mail.provider.com"]', process.env.SMTP_HOST)
await page.fill(
'[placeholder="user@provider.com"]',
process.env.SMTP_USERNAME
)
await page.fill('[type="password"]', process.env.SMTP_PASSWORD)
await page.fill('input[role="spinbutton"]', process.env.SMTP_PORT)
await page.fill('[placeholder="mail.provider.com"]', env.SMTP_HOST)
await page.fill('[placeholder="user@provider.com"]', env.SMTP_USERNAME)
await page.fill('[type="password"]', env.SMTP_PASSWORD)
await page.fill('input[role="spinbutton"]', env.SMTP_PORT.toString())
await expect(createButton).toBeEnabled()
await createButton.click()
await expect(
page.locator(`button >> text=${process.env.SMTP_USERNAME}`)
page.locator(`button >> text=${env.SMTP_USERNAME}`)
).toBeVisible()
await page.fill(

View File

@@ -7,6 +7,7 @@ import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/web
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { apiToken } from '@typebot.io/lib/playwright/databaseSetup'
import { env } from '@typebot.io/env'
test.describe('Builder', () => {
test('easy configuration should work', async ({ page }) => {
@@ -22,7 +23,7 @@ test.describe('Builder', () => {
await page.click('text=Configure...')
await page.fill(
'input[placeholder="Paste webhook URL..."]',
`${process.env.NEXTAUTH_URL}/api/mock/webhook-easy-config`
`${env.NEXTAUTH_URL}/api/mock/webhook-easy-config`
)
await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
@@ -45,7 +46,7 @@ test.describe('Builder', () => {
await page.click('text=Configure...')
await page.fill(
'input[placeholder="Paste webhook URL..."]',
`${process.env.NEXTAUTH_URL}/api/mock/webhook`
`${env.NEXTAUTH_URL}/api/mock/webhook`
)
await page.click('text=Advanced configuration')
await page.getByRole('button', { name: 'GET' }).click()

View File

@@ -1,7 +1,7 @@
import { fetcher } from '@/helpers/fetcher'
import { Invitation } from '@typebot.io/prisma'
import useSWR from 'swr'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
export const useInvitations = ({
typebotId,
@@ -14,7 +14,7 @@ export const useInvitations = ({
typebotId ? `/api/typebots/${typebotId}/invitations` : null,
fetcher,
{
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
dedupingInterval: env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
}
)
if (error) onError(error)

View File

@@ -1,5 +1,6 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { env } from '@typebot.io/env'
export const getAppVersionProcedure = publicProcedure.query(async () => {
return { commitSha: process.env.VERCEL_GIT_COMMIT_SHA }
return { commitSha: env.VERCEL_GIT_COMMIT_SHA }
})

View File

@@ -2,7 +2,7 @@ import { fetcher } from '@/helpers/fetcher'
import { DashboardFolder } from '@typebot.io/prisma'
import { stringify } from 'qs'
import useSWR from 'swr'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
export const useFolders = ({
parentId,
@@ -18,7 +18,7 @@ export const useFolders = ({
workspaceId ? `/api/folders?${params}` : null,
fetcher,
{
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
dedupingInterval: env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
}
)
if (error) onError(error)

View File

@@ -13,7 +13,7 @@ import {
Text,
} from '@chakra-ui/react'
import { Plan } from '@typebot.io/prisma'
import { isDefined, getViewerUrl, isNotDefined, env } from '@typebot.io/lib'
import { isDefined, isNotDefined } from '@typebot.io/lib'
import { isPublicDomainAvailableQuery } from '../queries/isPublicDomainAvailableQuery'
import { EditableUrl } from './EditableUrl'
import { integrationsList } from './embeds/EmbedButton'
@@ -25,6 +25,7 @@ import { CustomDomainsDropdown } from '@/features/customDomains/components/Custo
import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
import { useI18n } from '@/locales'
import { env } from '@typebot.io/env'
export const SharePage = () => {
const t = useI18n()
@@ -97,7 +98,7 @@ export const SharePage = () => {
</Heading>
{typebot && (
<EditableUrl
hostname={getViewerUrl() ?? 'https://typebot.io'}
hostname={env.NEXT_PUBLIC_VIEWER_URL[0]}
pathname={publicId}
isValid={checkIfPublicIdIsValid}
onPathnameChange={handlePublicIdChange}
@@ -120,7 +121,7 @@ export const SharePage = () => {
</HStack>
)}
{isNotDefined(typebot?.customDomain) &&
env('VERCEL_VIEWER_PROJECT_NAME') ? (
env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME ? (
<>
{isProPlan(workspace) ? (
<CustomDomainsDropdown

View File

@@ -18,7 +18,7 @@ import {
Text,
Stack,
} from '@chakra-ui/react'
import { env, getViewerUrl } from '@typebot.io/lib'
import { getViewerUrl } from '@typebot.io/lib'
import { ModalProps } from '../EmbedButton'
export const FlutterFlowModal = ({
@@ -51,16 +51,12 @@ export const FlutterFlowModal = ({
<InputGroup size="sm">
<Input
type={'text'}
defaultValue={`${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
defaultValue={`${getViewerUrl()}/${publicId}`}
/>
<InputRightElement width="60px">
<CopyButton
size="sm"
textToCopy={`${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
textToCopy={`${getViewerUrl()}/${publicId}`}
/>
</InputRightElement>
</InputGroup>

View File

@@ -1,6 +1,6 @@
import { FlexProps } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { env, getViewerUrl } from '@typebot.io/lib'
import { getViewerUrl } from '@typebot.io/lib'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import prettier from 'prettier/standalone'
import parserHtml from 'prettier/parser-html'
@@ -13,9 +13,7 @@ type Props = {
export const IframeSnippet = ({ widthLabel, heightLabel }: Props) => {
const { typebot } = useTypebot()
const src = `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
typebot?.publicId
}`
const src = `${getViewerUrl()}/${typebot?.publicId}`
const code = prettier.format(
`<iframe src="${src}" style="border: none; width='${widthLabel}'; height='${heightLabel}'"></iframe>`,
{ parser: 'html', plugins: [parserHtml] }

View File

@@ -18,7 +18,7 @@ import {
Text,
Stack,
} from '@chakra-ui/react'
import { env, getViewerUrl } from '@typebot.io/lib'
import { getViewerUrl } from '@typebot.io/lib'
import { ModalProps } from '../EmbedButton'
export const NotionModal = ({
@@ -49,16 +49,12 @@ export const NotionModal = ({
<InputGroup size="sm">
<Input
type={'text'}
defaultValue={`${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
defaultValue={`${getViewerUrl()}/${publicId}`}
/>
<InputRightElement width="60px">
<CopyButton
size="sm"
textToCopy={`${
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
}/${publicId}`}
textToCopy={`${getViewerUrl()}/${publicId}`}
/>
</InputRightElement>
</InputGroup>

View File

@@ -12,7 +12,7 @@ import {
import { useState } from 'react'
import { StandardSettings } from '../../../settings/StandardSettings'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import { env, getViewerUrl } from '@typebot.io/lib'
import { getViewerUrl } from '@typebot.io/lib'
type Props = {
publicId: string
@@ -76,9 +76,7 @@ const parseWordpressShortcode = ({
publicId: string
}) => {
return `[typebot typebot="${publicId}"${
isCloudProdInstance
? ''
: ` host="${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}"`
isCloudProdInstance ? '' : ` host="${getViewerUrl()}"`
}${width ? ` width="${width}"` : ''}${height ? ` height="${height}"` : ''}]
`
}

View File

@@ -1,7 +1,7 @@
import { BotProps } from '@typebot.io/nextjs'
import parserBabel from 'prettier/parser-babel'
import prettier from 'prettier/standalone'
import { env, getViewerUrl, isDefined } from '@typebot.io/lib'
import { getViewerUrl, isDefined } from '@typebot.io/lib'
import { Typebot } from '@typebot.io/schemas'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import packageJson from '../../../../../../../../packages/embeds/js/package.json'
@@ -58,7 +58,7 @@ export const parseApiHost = (
customDomain: Typebot['customDomain'] | undefined
) => {
if (customDomain) return new URL(`https://${customDomain}`).origin
return env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
return getViewerUrl()
}
export const parseApiHostValue = (

View File

@@ -4,6 +4,7 @@ import { PostHog } from 'posthog-node'
import { TRPCError } from '@trpc/server'
import got from 'got'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { env } from '@typebot.io/env'
// Only used for the cloud version of Typebot. It's the way it processes telemetry events and inject it to thrid-party services.
export const processTelemetryEvent = authenticatedProcedure
@@ -26,17 +27,17 @@ export const processTelemetryEvent = authenticatedProcedure
})
)
.query(async ({ input: { events }, ctx: { user } }) => {
if (user.email !== process.env.ADMIN_EMAIL)
if (user.email !== env.ADMIN_EMAIL)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Only app admin can process telemetry events',
})
if (!process.env.POSTHOG_API_KEY)
if (!env.POSTHOG_API_KEY)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Server does not have POSTHOG_API_KEY configured',
})
const client = new PostHog(process.env.POSTHOG_API_KEY, {
const client = new PostHog(env.POSTHOG_API_KEY, {
host: 'https://eu.posthog.com',
})
@@ -65,11 +66,8 @@ export const processTelemetryEvent = authenticatedProcedure
groupKey: event.typebotId,
properties: { name: event.data.name },
})
if (
event.name === 'User created' &&
process.env.USER_CREATED_WEBHOOK_URL
) {
await got.post(process.env.USER_CREATED_WEBHOOK_URL, {
if (event.name === 'User created' && env.USER_CREATED_WEBHOOK_URL) {
await got.post(env.USER_CREATED_WEBHOOK_URL, {
json: {
email: event.data.email,
name: event.data.name ? event.data.name.split(' ')[0] : undefined,

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { Text, HStack } from '@chakra-ui/react'
import { env, isEmpty } from '@typebot.io/lib'
import { AutocompleteInput } from '@/components/inputs/AutocompleteInput'
import { env } from '@typebot.io/env'
type FontSelectorProps = {
activeFont?: string
@@ -20,11 +20,9 @@ export const FontSelector = ({
}, [])
const fetchPopularFonts = async () => {
if (isEmpty(env('GOOGLE_API_KEY'))) return []
if (!env.NEXT_PUBLIC_GOOGLE_API_KEY) return []
const response = await fetch(
`https://www.googleapis.com/webfonts/v1/webfonts?key=${env(
'GOOGLE_API_KEY'
)}&sort=popularity`
`https://www.googleapis.com/webfonts/v1/webfonts?key=${env.NEXT_PUBLIC_GOOGLE_API_KEY}&sort=popularity`
)
return (await response.json()).items.map(
(item: { family: string }) => item.family

View File

@@ -1,4 +1,5 @@
import prisma from '@/lib/prisma'
import { env } from '@typebot.io/env'
import { CollaboratorsOnTypebots, User } from '@typebot.io/prisma'
import { Typebot } from '@typebot.io/schemas'
@@ -9,7 +10,7 @@ export const isReadTypebotForbidden = async (
user: Pick<User, 'email' | 'id'>
) => {
if (
process.env.ADMIN_EMAIL === user.email ||
env.ADMIN_EMAIL === user.email ||
typebot.collaborators.find(
(collaborator) => collaborator.userId === user.id
)

View File

@@ -1,8 +1,9 @@
import { env } from '@typebot.io/env'
import { Plan } from '@typebot.io/prisma'
export const parseWorkspaceDefaultPlan = (userEmail: string) => {
if (process.env.ADMIN_EMAIL === userEmail) return Plan.UNLIMITED
const defaultPlan = process.env.DEFAULT_WORKSPACE_PLAN as Plan | undefined
if (env.ADMIN_EMAIL === userEmail) return Plan.UNLIMITED
const defaultPlan = env.DEFAULT_WORKSPACE_PLAN as Plan | undefined
if (defaultPlan && Object.values(Plan).includes(defaultPlan))
return defaultPlan
return Plan.FREE

View File

@@ -1,7 +1,7 @@
import { WorkspaceInvitation } from '@typebot.io/prisma'
import { fetcher } from '@/helpers/fetcher'
import useSWR from 'swr'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
import { Member } from '../types'
export const useMembers = ({ workspaceId }: { workspaceId?: string }) => {
@@ -9,7 +9,7 @@ export const useMembers = ({ workspaceId }: { workspaceId?: string }) => {
{ members: Member[]; invitations: WorkspaceInvitation[] },
Error
>(workspaceId ? `/api/workspaces/${workspaceId}/members` : null, fetcher, {
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
dedupingInterval: env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
})
return {
members: data?.members ?? [],

View File

@@ -7,14 +7,14 @@ import {
} from '@typebot.io/prisma'
import prisma from '@/lib/prisma'
import { NextApiResponse } from 'next'
import { env, isNotEmpty } from '@typebot.io/lib'
import { forbidden } from '@typebot.io/lib/api'
import { env } from '@typebot.io/env'
export const canWriteTypebots = (
typebotIds: string[] | string,
user: Pick<User, 'email' | 'id'>
): Prisma.TypebotWhereInput =>
isNotEmpty(env('E2E_TEST'))
env.NEXT_PUBLIC_E2E_TEST
? { id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds } }
: {
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
@@ -40,7 +40,7 @@ export const canReadTypebots = (
) => ({
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
workspace:
user.email === process.env.ADMIN_EMAIL || isNotEmpty(env('E2E_TEST'))
user.email === env.ADMIN_EMAIL || env.NEXT_PUBLIC_E2E_TEST
? undefined
: {
members: {

View File

@@ -1,3 +1,4 @@
import { env } from '@typebot.io/env'
import { Client } from 'minio'
export const deleteFilesFromBucket = async ({
@@ -5,32 +6,26 @@ export const deleteFilesFromBucket = async ({
}: {
urls: string[]
}): Promise<void> => {
if (
!process.env.S3_ENDPOINT ||
!process.env.S3_ACCESS_KEY ||
!process.env.S3_SECRET_KEY
)
if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
throw new Error(
'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY'
)
const useSSL =
process.env.S3_SSL && process.env.S3_SSL === 'false' ? false : true
const minioClient = new Client({
endPoint: process.env.S3_ENDPOINT,
port: process.env.S3_PORT ? parseInt(process.env.S3_PORT) : undefined,
useSSL,
accessKey: process.env.S3_ACCESS_KEY,
secretKey: process.env.S3_SECRET_KEY,
region: process.env.S3_REGION,
endPoint: env.S3_ENDPOINT,
port: env.S3_PORT,
useSSL: env.S3_SSL,
accessKey: env.S3_ACCESS_KEY,
secretKey: env.S3_SECRET_KEY,
region: env.S3_REGION,
})
const bucket = process.env.S3_BUCKET ?? 'typebot'
const bucket = env.S3_BUCKET ?? 'typebot'
return minioClient.removeObjects(
bucket,
urls
.filter((url) => url.includes(process.env.S3_ENDPOINT as string))
.filter((url) => url.includes(env.S3_ENDPOINT as string))
.map((url) => url.split(`/${bucket}/`)[1])
)
}

View File

@@ -4,11 +4,12 @@ import { GoogleSheetsCredentials } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { decrypt, encrypt } from '@typebot.io/lib/api'
import prisma from './prisma'
import { env } from '@typebot.io/env'
export const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
`${process.env.NEXTAUTH_URL}/api/credentials/google-sheets/callback`
env.GOOGLE_CLIENT_ID,
env.GOOGLE_CLIENT_SECRET,
`${env.NEXTAUTH_URL}/api/credentials/google-sheets/callback`
)
export const getAuthenticatedGoogleClient = async (

View File

@@ -2,6 +2,7 @@
* Instantiates a single instance PrismaClient and save it on the global object.
* @link https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices
*/
import { env } from '@typebot.io/env'
import { PrismaClient } from '@typebot.io/prisma'
const prismaGlobal = global as typeof global & {
@@ -11,10 +12,10 @@ const prismaGlobal = global as typeof global & {
const prisma: PrismaClient =
prismaGlobal.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
log: env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') {
if (env.NODE_ENV !== 'production') {
prismaGlobal.prisma = prisma
}

View File

@@ -2,10 +2,9 @@ import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'
import { createTRPCNext } from '@trpc/next'
import type { AppRouter } from '../helpers/server/routers/v1/trpcRouter'
import superjson from 'superjson'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
const getBaseUrl = () =>
typeof window !== 'undefined' ? '' : process.env.NEXTAUTH_URL
const getBaseUrl = () => (typeof window !== 'undefined' ? '' : env.NEXTAUTH_URL)
export const trpc = createTRPCNext<AppRouter>({
config() {
@@ -36,5 +35,5 @@ export const trpcVanilla = createTRPCProxyClient<AppRouter>({
})
export const defaultQueryOptions = {
refetchOnMount: env('E2E_TEST') === 'true',
refetchOnMount: env.NEXT_PUBLIC_E2E_TEST,
}

View File

@@ -12,7 +12,7 @@ const Document = () => (
/>
<meta name="google" content="notranslate" />
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/__env.js" />
<script src="/__ENV.js" />
</Head>
<body>
<ColorModeScript initialColorMode={customTheme.config.initialColorMode} />

View File

@@ -10,139 +10,114 @@ import { Provider } from 'next-auth/providers'
import { NextApiRequest, NextApiResponse } from 'next'
import { customAdapter } from '../../../features/auth/api/customAdapter'
import { User } from '@typebot.io/prisma'
import { env, getAtPath, isDefined, isNotEmpty } from '@typebot.io/lib'
import { getAtPath, isDefined } from '@typebot.io/lib'
import { mockedUser } from '@/features/auth/mockedUser'
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis/nodejs'
import got from 'got'
import { env } from '@typebot.io/env'
const providers: Provider[] = []
let rateLimit: Ratelimit | undefined
if (
process.env.UPSTASH_REDIS_REST_URL &&
process.env.UPSTASH_REDIS_REST_TOKEN
) {
if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) {
rateLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(1, '60 s'),
})
}
if (
isNotEmpty(process.env.GITHUB_CLIENT_ID) &&
isNotEmpty(process.env.GITHUB_CLIENT_SECRET)
)
if (env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET)
providers.push(
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
})
)
if (isNotEmpty(env('SMTP_FROM')) && process.env.SMTP_AUTH_DISABLED !== 'true')
if (env.NEXT_PUBLIC_SMTP_FROM && env.SMTP_AUTH_DISABLED)
providers.push(
EmailProvider({
server: {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 25,
secure: process.env.SMTP_SECURE
? process.env.SMTP_SECURE === 'true'
: false,
host: env.SMTP_HOST,
port: env.SMTP_PORT ? Number(env.SMTP_PORT) : 25,
secure: env.SMTP_SECURE ? env.SMTP_SECURE : false,
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
user: env.SMTP_USERNAME,
pass: env.SMTP_PASSWORD,
},
},
from: env('SMTP_FROM'),
from: env.NEXT_PUBLIC_SMTP_FROM,
sendVerificationRequest,
})
)
if (
isNotEmpty(process.env.GOOGLE_CLIENT_ID) &&
isNotEmpty(process.env.GOOGLE_CLIENT_SECRET)
)
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET)
providers.push(
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
})
)
if (
isNotEmpty(process.env.FACEBOOK_CLIENT_ID) &&
isNotEmpty(process.env.FACEBOOK_CLIENT_SECRET)
)
if (env.FACEBOOK_CLIENT_ID && env.FACEBOOK_CLIENT_SECRET)
providers.push(
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
clientId: env.FACEBOOK_CLIENT_ID,
clientSecret: env.FACEBOOK_CLIENT_SECRET,
})
)
if (
isNotEmpty(process.env.GITLAB_CLIENT_ID) &&
isNotEmpty(process.env.GITLAB_CLIENT_SECRET)
) {
const BASE_URL = process.env.GITLAB_BASE_URL || 'https://gitlab.com'
if (env.GITLAB_CLIENT_ID && env.GITLAB_CLIENT_SECRET) {
const BASE_URL = env.GITLAB_BASE_URL || 'https://gitlab.com'
providers.push(
GitlabProvider({
clientId: process.env.GITLAB_CLIENT_ID,
clientSecret: process.env.GITLAB_CLIENT_SECRET,
clientId: env.GITLAB_CLIENT_ID,
clientSecret: env.GITLAB_CLIENT_SECRET,
authorization: `${BASE_URL}/oauth/authorize?scope=read_api`,
token: `${BASE_URL}/oauth/token`,
userinfo: `${BASE_URL}/api/v4/user`,
name: process.env.GITLAB_NAME || 'GitLab',
name: env.GITLAB_NAME || 'GitLab',
})
)
}
if (
isNotEmpty(process.env.AZURE_AD_CLIENT_ID) &&
isNotEmpty(process.env.AZURE_AD_CLIENT_SECRET) &&
isNotEmpty(process.env.AZURE_AD_TENANT_ID)
env.AZURE_AD_CLIENT_ID &&
env.AZURE_AD_CLIENT_SECRET &&
env.AZURE_AD_TENANT_ID
) {
providers.push(
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
clientId: env.AZURE_AD_CLIENT_ID,
clientSecret: env.AZURE_AD_CLIENT_SECRET,
tenantId: env.AZURE_AD_TENANT_ID,
})
)
}
if (isNotEmpty(process.env.CUSTOM_OAUTH_WELL_KNOWN_URL)) {
if (env.CUSTOM_OAUTH_WELL_KNOWN_URL) {
providers.push({
id: 'custom-oauth',
name: process.env.CUSTOM_OAUTH_NAME ?? 'Custom OAuth',
name: env.CUSTOM_OAUTH_NAME ?? 'Custom OAuth',
type: 'oauth',
authorization: {
params: {
scope: process.env.CUSTOM_OAUTH_SCOPE ?? 'openid profile email',
scope: env.CUSTOM_OAUTH_SCOPE ?? 'openid profile email',
},
},
clientId: process.env.CUSTOM_OAUTH_CLIENT_ID,
clientSecret: process.env.CUSTOM_OAUTH_CLIENT_SECRET,
wellKnown: process.env.CUSTOM_OAUTH_WELL_KNOWN_URL,
clientId: env.CUSTOM_OAUTH_CLIENT_ID,
clientSecret: env.CUSTOM_OAUTH_CLIENT_SECRET,
wellKnown: env.CUSTOM_OAUTH_WELL_KNOWN_URL,
profile(profile) {
return {
id: getAtPath(profile, process.env.CUSTOM_OAUTH_USER_ID_PATH ?? 'id'),
name: getAtPath(
profile,
process.env.CUSTOM_OAUTH_USER_NAME_PATH ?? 'name'
),
email: getAtPath(
profile,
process.env.CUSTOM_OAUTH_USER_EMAIL_PATH ?? 'email'
),
image: getAtPath(
profile,
process.env.CUSTOM_OAUTH_USER_IMAGE_PATH ?? 'image'
),
id: getAtPath(profile, env.CUSTOM_OAUTH_USER_ID_PATH ?? 'id'),
name: getAtPath(profile, env.CUSTOM_OAUTH_USER_NAME_PATH ?? 'name'),
email: getAtPath(profile, env.CUSTOM_OAUTH_USER_EMAIL_PATH ?? 'email'),
image: getAtPath(profile, env.CUSTOM_OAUTH_USER_IMAGE_PATH ?? 'image'),
} as User
},
})
@@ -150,16 +125,14 @@ if (isNotEmpty(process.env.CUSTOM_OAUTH_WELL_KNOWN_URL)) {
export const authOptions: AuthOptions = {
adapter: customAdapter(prisma),
secret: process.env.ENCRYPTION_SECRET,
secret: env.ENCRYPTION_SECRET,
providers,
session: {
strategy: 'database',
},
pages: {
signIn: '/signin',
newUser: process.env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID
? '/onboarding'
: undefined,
newUser: env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID ? '/onboarding' : undefined,
},
callbacks: {
session: async ({ session, user }) => {
@@ -181,7 +154,7 @@ export const authOptions: AuthOptions = {
if (disposableEmailDomains.includes(user.email.split('@')[1]))
return false
}
if (process.env.DISABLE_SIGNUP === 'true' && isNewUser && user.email) {
if (env.DISABLE_SIGNUP && isNewUser && user.email) {
const { invitations, workspaceInvitations } =
await getNewUserInvitations(prisma, user.email)
if (invitations.length === 0 && workspaceInvitations.length === 0)
@@ -201,7 +174,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const isMockingSession =
req.method === 'GET' &&
req.url === '/api/auth/session' &&
env('E2E_TEST') === 'true'
env.NEXT_PUBLIC_E2E_TEST
if (isMockingSession) return res.send({ user: mockedUser })
const requestIsFromCompanyFirewall = req.method === 'HEAD'
if (requestIsFromCompanyFirewall) return res.status(200).end()
@@ -248,7 +221,7 @@ const getUserGroups = async (account: Account): Promise<string[]> => {
): Promise<{ full_path: string }[]> => {
const res = await fetch(
`${
process.env.GITLAB_BASE_URL || 'https://gitlab.com'
env.GITLAB_BASE_URL || 'https://gitlab.com'
}/api/v4/groups?per_page=100&page=${page}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
)
@@ -269,7 +242,7 @@ const getUserGroups = async (account: Account): Promise<string[]> => {
const getRequiredGroups = (provider: string): string[] => {
switch (provider) {
case 'gitlab':
return process.env.GITLAB_REQUIRED_GROUPS?.split(',') || []
return env.GITLAB_REQUIRED_GROUPS ?? []
default:
return []
}

View File

@@ -6,6 +6,7 @@ import { stringify } from 'querystring'
import { badRequest, encrypt, notAuthenticated } from '@typebot.io/lib/api'
import { oauth2Client } from '@/lib/googleSheets'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { env } from '@typebot.io/env'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
@@ -49,9 +50,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
data: credentials,
})
const queryParams = stringify({ blockId, credentialsId })
res.redirect(
`${redirectUrl}?${queryParams}` ?? `${process.env.NEXTAUTH_URL}`
)
res.redirect(`${redirectUrl}?${queryParams}` ?? `${env.NEXTAUTH_URL}`)
}
}

View File

@@ -7,6 +7,7 @@ import {
} from '@typebot.io/lib/api'
import { got } from 'got'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { env } from '@typebot.io/env'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
@@ -31,8 +32,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const deleteDomainOnVercel = (name: string) =>
got.delete({
url: `https://api.vercel.com/v8/projects/${process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${process.env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
url: `https://api.vercel.com/v8/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` },
})
export default handler

View File

@@ -6,6 +6,7 @@ import {
methodNotAllowed,
notAuthenticated,
} from '@typebot.io/lib/api'
import { env } from '@typebot.io/env'
const handler = async (
req: NextApiRequest,
@@ -16,11 +17,7 @@ const handler = async (
const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)
if (
!process.env.S3_ENDPOINT ||
!process.env.S3_ACCESS_KEY ||
!process.env.S3_SECRET_KEY
)
if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
return badRequest(
res,
'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY'

View File

@@ -8,10 +8,11 @@ import { Plan, WorkspaceRole } from '@typebot.io/prisma'
import { RequestHandler } from 'next/dist/server/next'
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
import { Settings } from '@typebot.io/schemas'
import { env } from '@typebot.io/env'
if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET)
if (!env.STRIPE_SECRET_KEY || !env.STRIPE_WEBHOOK_SECRET)
throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
@@ -19,7 +20,7 @@ const cors = Cors({
allowMethods: ['POST', 'HEAD'],
})
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string
const webhookSecret = env.STRIPE_WEBHOOK_SECRET as string
export const config = {
api: {

View File

@@ -13,8 +13,8 @@ import {
notAuthenticated,
} from '@typebot.io/lib/api'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { env } from '@typebot.io/lib'
import { sendGuestInvitationEmail } from '@typebot.io/emails'
import { env } from '@typebot.io/env'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
@@ -80,11 +80,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await prisma.invitation.create({
data: { email: email.toLowerCase().trim(), type, typebotId },
})
if (env('E2E_TEST') !== 'true')
if (!env.NEXT_PUBLIC_E2E_TEST)
await sendGuestInvitationEmail({
to: email,
hostEmail: user.email ?? '',
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${typebot.workspaceId}`,
url: `${env.NEXTAUTH_URL}/typebots?workspaceId=${typebot.workspaceId}`,
guestEmail: email.toLowerCase(),
typebotName: typebot.name,
workspaceName: typebot.workspace?.name ?? '',

View File

@@ -8,8 +8,8 @@ import {
} from '@typebot.io/lib/api'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { sendWorkspaceMemberInvitationEmail } from '@typebot.io/emails'
import { env } from '@typebot.io/lib'
import { isSeatsLimitReached } from '@typebot.io/lib/pricing'
import { env } from '@typebot.io/env'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
@@ -52,12 +52,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
userId: existingUser.id,
},
})
if (env('E2E_TEST') !== 'true')
if (!env.NEXT_PUBLIC_E2E_TEST)
await sendWorkspaceMemberInvitationEmail({
to: data.email,
workspaceName: workspace.name,
guestEmail: data.email,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
url: `${env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
hostEmail: user.email ?? '',
})
return res.send({
@@ -71,12 +71,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
})
} else {
const invitation = await prisma.workspaceInvitation.create({ data })
if (env('E2E_TEST') !== 'true')
if (!env.NEXT_PUBLIC_E2E_TEST)
await sendWorkspaceMemberInvitationEmail({
to: data.email,
workspaceName: workspace.name,
guestEmail: data.email,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
url: `${env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
hostEmail: user.email ?? '',
})
return res.send({ invitation })

View File

@@ -4,6 +4,7 @@ import { isNotDefined } from '@typebot.io/lib'
import { sign } from 'jsonwebtoken'
import { getServerSession } from 'next-auth'
import { authOptions } from './api/auth/[...nextauth]'
import { env } from '@typebot.io/env'
export default function Page() {
return null
@@ -28,7 +29,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}
const createSSOToken = (user: User) => {
if (!process.env.SLEEKPLAN_SSO_KEY) return
if (!env.SLEEKPLAN_SSO_KEY) return
const userData = {
mail: user.email,
id: user.id,
@@ -36,5 +37,5 @@ const createSSOToken = (user: User) => {
img: user.image,
}
return sign(userData, process.env.SLEEKPLAN_SSO_KEY, { algorithm: 'HS256' })
return sign(userData, env.SLEEKPLAN_SSO_KEY, { algorithm: 'HS256' })
}

View File

@@ -4,6 +4,7 @@ import { isNotDefined } from '@typebot.io/lib'
import { sign } from 'jsonwebtoken'
import { authOptions } from '../api/auth/[...nextauth]'
import { GetServerSidePropsContext } from 'next'
import { env } from '@typebot.io/env'
export default function Page() {
return null
@@ -29,7 +30,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}
const createSSOToken = (user: User) => {
if (!process.env.SLEEKPLAN_SSO_KEY) return
if (!env.SLEEKPLAN_SSO_KEY) return
const userData = {
mail: user.email,
id: user.id,
@@ -37,5 +38,5 @@ const createSSOToken = (user: User) => {
img: user.image,
}
return sign(userData, process.env.SLEEKPLAN_SSO_KEY, { algorithm: 'HS256' })
return sign(userData, env.SLEEKPLAN_SSO_KEY, { algorithm: 'HS256' })
}

View File

@@ -7,10 +7,11 @@ import {
} from '@typebot.io/prisma'
import Stripe from 'stripe'
import { proWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'
import { env } from '@typebot.io/env'
const prisma = new PrismaClient()
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '', {
const stripe = new Stripe(env.STRIPE_SECRET_KEY ?? '', {
apiVersion: '2022-11-15',
})