🧑‍💻 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

@@ -1,5 +1,5 @@
import React from 'react'
import { getViewerUrl, isEmpty } from '@typebot.io/lib'
import { getViewerUrl } from '@typebot.io/lib'
export const ErrorPage = ({ error }: { error: Error }) => {
return (
@@ -13,7 +13,7 @@ export const ErrorPage = ({ error }: { error: Error }) => {
padding: '0 1rem',
}}
>
{isEmpty(getViewerUrl()) ? (
{!getViewerUrl() ? (
<>
<h1 style={{ fontWeight: 'bold', fontSize: '30px' }}>
NEXT_PUBLIC_VIEWER_URL is missing

View File

@@ -11,6 +11,7 @@ import {
import { byId, isDefined } from '@typebot.io/lib'
import { z } from 'zod'
import { generatePresignedUrl } from '@typebot.io/lib/api/storage'
import { env } from '@typebot.io/env'
export const getUploadUrl = publicProcedure
.meta({
@@ -39,11 +40,7 @@ export const getUploadUrl = publicProcedure
})
)
.query(async ({ input: { typebotId, blockId, filePath, fileType } }) => {
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 TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message:

View File

@@ -10,6 +10,7 @@ import {
} from '@typebot.io/lib/playwright/databaseActions'
import { getTestAsset } from '@/test/utils/playwright'
import { Plan } from '@typebot.io/prisma'
import { env } from '@typebot.io/env'
const THREE_GIGABYTES = 3 * 1024 * 1024 * 1024
@@ -30,7 +31,7 @@ test('should work as expected', async ({ page, browser }) => {
await expect(page.locator(`text="3"`)).toBeVisible()
await page.locator('text="Upload 3 files"').click()
await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await expect(page.getByRole('link', { name: 'api.json' })).toHaveAttribute(
'href',
/.+\/api\.json/
@@ -52,7 +53,7 @@ test('should work as expected', async ({ page, browser }) => {
const file = readFileSync(downloadPath as string).toString()
const { data } = parse(file)
expect(data).toHaveLength(2)
expect((data[1] as unknown[])[1]).toContain(process.env.S3_ENDPOINT)
expect((data[1] as unknown[])[1]).toContain(env.S3_ENDPOINT)
const urls = (
await Promise.all(
@@ -110,7 +111,7 @@ test.describe('Storage limit is reached', () => {
await page.evaluate(() =>
window.localStorage.setItem('workspaceId', 'starterWorkspace')
)
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await expect(page.locator('text="150%"')).toBeVisible()
})
})

View File

@@ -2,6 +2,7 @@ import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { extractVariablesFromText } from '@/features/variables/extractVariablesFromText'
import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType'
import { parseVariables } from '@/features/variables/parseVariables'
import { env } from '@typebot.io/env'
import { isDefined } from '@typebot.io/lib'
import {
ChatwootBlock,
@@ -30,7 +31,7 @@ const parseChatwootOpenCode = ({
const openChatwoot = `${parseSetUserCode(user, resultId)}
window.$chatwoot.setCustomAttributes({
typebot_result_url: "${
process.env.NEXTAUTH_URL
env.NEXTAUTH_URL
}/typebots/${typebotId}/results?id=${resultId}",
});
window.$chatwoot.toggle("open");

View File

@@ -1,14 +1,16 @@
import { env } from '@typebot.io/env'
export const defaultTransportOptions = {
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE ? process.env.SMTP_SECURE === 'true' : false,
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_SECURE,
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
user: env.SMTP_USERNAME,
pass: env.SMTP_PASSWORD,
},
}
export const defaultFrom = {
name: process.env.SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
email: process.env.SMTP_FROM?.match(/<(.*)>/)?.pop(),
name: env.NEXT_PUBLIC_SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
email: env.NEXT_PUBLIC_SMTP_FROM?.match(/<(.*)>/)?.pop(),
}

View File

@@ -20,6 +20,7 @@ import { decrypt } from '@typebot.io/lib/api'
import { defaultFrom, defaultTransportOptions } from './constants'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
import { env } from '@typebot.io/env'
export const executeSendEmailBlock = async (
state: SessionState,
@@ -186,7 +187,7 @@ const getEmailInfo = async (
if (credentialsId === 'default')
return {
host: defaultTransportOptions.host,
port: defaultTransportOptions.port,
port: defaultTransportOptions.port ?? 22,
username: defaultTransportOptions.auth.user,
password: defaultTransportOptions.auth.pass,
isTlsEnabled: undefined,
@@ -226,7 +227,7 @@ const getEmailBody = async ({
return {
html: render(
<DefaultBotNotificationEmail
resultsUrl={`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
resultsUrl={`${env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
answers={omit(answers, 'submittedAt')}
/>
).html,

View File

@@ -4,6 +4,7 @@ import { createId } from '@paralleldrive/cuid2'
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
import { getTestAsset } from '@/test/utils/playwright'
import { SmtpCredentials } from '@typebot.io/schemas'
import { env } from '@typebot.io/env'
export const mockSmtpCredentials: SmtpCredentials['data'] = {
from: {
@@ -34,7 +35,7 @@ test('should send an email', async ({ page }) => {
await page.goto(`/${typebotId}-public`)
await page.locator('text=Send email').click()
await expect(page.getByText('Email sent!')).toBeVisible()
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await page.click('text="See logs"')
await expect(page.locator('text="Email successfully sent"')).toBeVisible()
})

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 { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
@@ -33,7 +34,7 @@ test('should work as expected', async ({ page }) => {
await page.locator('input').fill('Hello there!')
await page.locator('input').press('Enter')
await expect(page.getByText('Cheers!')).toBeVisible()
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await expect(page.locator('text=Hello there!')).toBeVisible()
})

View File

@@ -16,7 +16,7 @@ import {
Variable,
VariableWithValue,
} from '@typebot.io/schemas'
import { env, isDefined, isNotEmpty, omit } from '@typebot.io/lib'
import { isDefined, isNotEmpty, omit } from '@typebot.io/lib'
import { prefillVariables } from '@/features/variables/prefillVariables'
import { injectVariablesFromExistingResult } from '@/features/variables/injectVariablesFromExistingResult'
import { deepParseVariables } from '@/features/variables/deepParseVariable'
@@ -30,6 +30,7 @@ import { findTypebot } from '../queries/findTypebot'
import { findPublicTypebot } from '../queries/findPublicTypebot'
import { findResult } from '../queries/findResult'
import { createId } from '@paralleldrive/cuid2'
import { env } from '@typebot.io/env'
export const sendMessage = publicProcedure
.meta({
@@ -257,7 +258,7 @@ const getTypebot = async (
userId?: string
): Promise<StartTypebot> => {
if (typeof typebot !== 'string') return typebot
if (isPreview && !userId && env('E2E_TEST') !== 'true')
if (isPreview && !userId && !env.NEXT_PUBLIC_E2E_TEST)
throw new TRPCError({
code: 'UNAUTHORIZED',
message:

View File

@@ -2,6 +2,7 @@ import { getTestAsset } from '@/test/utils/playwright'
import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
import { env } from '@typebot.io/env'
test('Big groups should work as expected', async ({ page }) => {
const typebotId = createId()
@@ -15,7 +16,7 @@ test('Big groups should work as expected', async ({ page }) => {
await page.locator('input').fill('26')
await page.locator('input').press('Enter')
await page.getByRole('button', { name: 'Yes' }).click()
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await expect(page.locator('text="Baptiste"')).toBeVisible()
await expect(page.locator('text="26"')).toBeVisible()
await expect(page.locator('text="Yes"')).toBeVisible()

View File

@@ -4,7 +4,7 @@ import {
User,
WorkspaceRole,
} from '@typebot.io/prisma'
import { env } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
const parseWhereFilter = (
typebotIds: string[] | string,
@@ -24,8 +24,8 @@ const parseWhereFilter = (
{
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
workspace:
(type === 'read' && user.email === process.env.ADMIN_EMAIL) ||
env('E2E_TEST') === 'true'
(type === 'read' && user.email === env.ADMIN_EMAIL) ||
env.NEXT_PUBLIC_E2E_TEST
? undefined
: {
members: {

View File

@@ -1,2 +1,3 @@
export const isPlaneteScale = () =>
process.env.DATABASE_URL?.includes('pscale_pw')
import { env } from '@typebot.io/env'
export const isPlaneteScale = () => env.DATABASE_URL?.includes('pscale_pw')

View File

@@ -1,3 +1,3 @@
import { isDefined } from '@typebot.io/lib/utils'
import { env } from '@typebot.io/env'
export const isVercel = () => isDefined(process.env.NEXT_PUBLIC_VERCEL_ENV)
export const isVercel = () => env.NEXT_PUBLIC_VERCEL_ENV

View File

@@ -4,6 +4,7 @@ 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 getAuthenticatedGoogleClient = async (
credentialsId: string
@@ -18,9 +19,9 @@ export const getAuthenticatedGoogleClient = async (
)) as GoogleSheetsCredentials['data']
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`
)
oauth2Client.setCredentials(data)
oauth2Client.on('tokens', updateTokens(credentialsId, data))

View File

@@ -1,9 +1,10 @@
import { env } from '@typebot.io/env'
import { PrismaClient } from '@typebot.io/prisma'
declare const global: { prisma: PrismaClient }
let prisma: PrismaClient
if (process.env.NODE_ENV === 'production') {
if (env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.prisma) {

View File

@@ -2,10 +2,11 @@ import { IncomingMessage } from 'http'
import { ErrorPage } from '@/components/ErrorPage'
import { NotFoundPage } from '@/components/NotFoundPage'
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
import { env, getViewerUrl, isNotDefined } from '@typebot.io/lib'
import { isNotDefined } from '@typebot.io/lib'
import prisma from '../lib/prisma'
import { TypebotPageProps, TypebotPageV2 } from '@/components/TypebotPageV2'
import { TypebotPageV3, TypebotV3PageProps } from '@/components/TypebotPageV3'
import { env } from '@typebot.io/env'
// Browsers that doesn't support ES modules and/or web components
const incompatibleBrowsers = [
@@ -24,7 +25,7 @@ const incompatibleBrowsers = [
]
const log = (message: string) => {
if (process.env.DEBUG !== 'true') return
if (!env.DEBUG) return
console.log(`[DEBUG] ${message}`)
}
@@ -41,19 +42,18 @@ export const getServerSideProps: GetServerSideProps = async (
log(`forwardedHost: ${forwardedHost}`)
try {
if (!host) return { props: {} }
const viewerUrls = (getViewerUrl({ returnAll: true }) ?? '').split(',')
const viewerUrls = env.NEXT_PUBLIC_VIEWER_URL
log(`viewerUrls: ${viewerUrls}`)
const isMatchingViewerUrl =
env('E2E_TEST') === 'true'
? true
: viewerUrls.some(
(url) =>
host.split(':')[0].includes(url.split('//')[1].split(':')[0]) ||
(forwardedHost &&
forwardedHost
.split(':')[0]
.includes(url.split('//')[1].split(':')[0]))
)
const isMatchingViewerUrl = env.NEXT_PUBLIC_E2E_TEST
? true
: viewerUrls.some(
(url) =>
host.split(':')[0].includes(url.split('//')[1].split(':')[0]) ||
(forwardedHost &&
forwardedHost
.split(':')[0]
.includes(url.split('//')[1].split(':')[0]))
)
log(`isMatchingViewerUrl: ${isMatchingViewerUrl}`)
const customDomain = `${forwardedHost ?? host}${
pathname === '/' ? '' : pathname

View File

@@ -4,7 +4,7 @@ import { Html, Head, Main, NextScript } from 'next/document'
const Document = () => (
<Html translate="no">
<Head>
<script src="/__env.js" />
<script src="/__ENV.js" />
</Head>
<body>
<Main />

View File

@@ -1,5 +1,6 @@
import { getChatCompletionStream } from '@/features/blocks/integrations/openai/getChatCompletionStream'
import { connect } from '@planetscale/database'
import { env } from '@typebot.io/env'
import { IntegrationBlockType, SessionState } from '@typebot.io/schemas'
import { StreamingTextResponse } from 'ai'
import { ChatCompletionRequestMessage } from 'openai'
@@ -29,7 +30,7 @@ const handler = async (req: Request) => {
if (!messages) return new Response('No messages provided', { status: 400 })
const conn = connect({ url: process.env.DATABASE_URL })
const conn = connect({ url: env.DATABASE_URL })
const chatSession = await conn.execute(
'select state from ChatSession where id=?',

View File

@@ -17,22 +17,23 @@ import { render } from '@faire/mjml-react/utils/render'
import prisma from '@/lib/prisma'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { env } from '@typebot.io/env'
const cors = initMiddleware(Cors())
const defaultTransportOptions = {
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE ? process.env.SMTP_SECURE === 'true' : false,
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_SECURE,
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
user: env.SMTP_USERNAME,
pass: env.SMTP_PASSWORD,
},
}
const defaultFrom = {
name: process.env.SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
email: process.env.SMTP_FROM?.match(/<(.*)>/)?.pop(),
name: env.NEXT_PUBLIC_SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
email: env.NEXT_PUBLIC_SMTP_FROM?.match(/<(.*)>/)?.pop(),
}
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@@ -159,7 +160,7 @@ const getEmailInfo = async (
if (credentialsId === 'default')
return {
host: defaultTransportOptions.host,
port: defaultTransportOptions.port,
port: defaultTransportOptions.port as number,
username: defaultTransportOptions.auth.user,
password: defaultTransportOptions.auth.pass,
isTlsEnabled: undefined,
@@ -213,7 +214,7 @@ const getEmailBody = async ({
return {
html: render(
<DefaultBotNotificationEmail
resultsUrl={`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
resultsUrl={`${env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
answers={omit(answers, 'submittedAt')}
/>
).html,