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

@@ -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 })