From 29f2da73798d6c0aa3cbde505973b7ec1c14ba84 Mon Sep 17 00:00:00 2001 From: MarconLP <13001502+MarconLP@users.noreply.github.com> Date: Wed, 3 May 2023 21:35:58 +0200 Subject: [PATCH] make non-core env variables optional --- .env.example | 16 +++----- src/components/CrispChat.tsx | 4 +- src/env.mjs | 18 ++++----- src/pages/_app.tsx | 10 ++--- src/pages/api/webhooks/stripe.ts | 8 +++- src/server/api/routers/stripe.ts | 20 +++++++-- src/server/api/routers/video.ts | 36 ++++++++--------- src/server/api/trpc.ts | 8 ++-- src/server/auth.ts | 58 ++++++++++++++------------- src/server/posthog.ts | 8 ++-- src/server/rateLimit.ts | 17 ++++---- src/server/stripe-webhook-handlers.ts | 18 ++++----- src/server/stripe.ts | 9 +++-- 13 files changed, 128 insertions(+), 102 deletions(-) diff --git a/.env.example b/.env.example index 644a2e4..3f48cfc 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,9 @@ -# Prisma -# https://www.prisma.io/docs/reference/database-reference/connection-urls#env +# Database DATABASE_URL="" -# Next Auth -# You can generate a new secret on the command line with: -# openssl rand -base64 32 -# https://next-auth.js.org/configuration/options#secret +# Authjs +NEXTAUTH_URL="http://localhost:3000/" NEXTAUTH_SECRET="" -NEXTAUTH_URL="http://localhost:3000" - -# Next Auth Provider GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_SECRET="" GITHUB_ID="" @@ -38,10 +32,10 @@ NEXT_PUBLIC_CRISP_WEBSITE_ID="" # posthog NEXT_PUBLIC_POSTHOG_KEY="" -NEXT_PUBLIC_POSTHOG_HOST="" +NEXT_PUBLIC_POSTHOG_HOST="https://app.posthog.com" NEXT_PUBLIC_POSTHOG_PROXY_HOST="/ioafe" POSTHOG_PROXY_PATH="ioafe" # redis for ratelimiting UPSTASH_REDIS_REST_URL="" -UPSTASH_REDIS_REST_TOKEN="=" \ No newline at end of file +UPSTASH_REDIS_REST_TOKEN="" \ No newline at end of file diff --git a/src/components/CrispChat.tsx b/src/components/CrispChat.tsx index 576ae98..e4f1e4f 100644 --- a/src/components/CrispChat.tsx +++ b/src/components/CrispChat.tsx @@ -4,7 +4,9 @@ import { env } from "~/env.mjs"; export default function CrispChat() { useEffect(() => { - Crisp.configure(env.NEXT_PUBLIC_CRISP_WEBSITE_ID); + if (env.NEXT_PUBLIC_CRISP_WEBSITE_ID) { + Crisp.configure(env.NEXT_PUBLIC_CRISP_WEBSITE_ID); + } }, []); return null; diff --git a/src/env.mjs b/src/env.mjs index 73c5a4d..24934e4 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -28,13 +28,13 @@ const server = z.object({ AWS_KEY_ID: z.string(), AWS_SECRET_ACCESS_KEY: z.string(), AWS_BUCKET_NAME: z.string(), - STRIPE_SECRET_KEY: z.string(), - STRIPE_WEBHOOK_SECRET: z.string(), - STRIPE_MONTHLY_PRICE_ID: z.string(), - STRIPE_ANNUAL_PRICE_ID: z.string(), + STRIPE_SECRET_KEY: z.string().nullish(), + STRIPE_WEBHOOK_SECRET: z.string().nullish(), + STRIPE_MONTHLY_PRICE_ID: z.string().nullish(), + STRIPE_ANNUAL_PRICE_ID: z.string().nullish(), POSTHOG_PROXY_PATH: z.string(), - UPSTASH_REDIS_REST_URL: z.string(), - UPSTASH_REDIS_REST_TOKEN: z.string(), + UPSTASH_REDIS_REST_URL: z.string().nullish(), + UPSTASH_REDIS_REST_TOKEN: z.string().nullish(), }); /** @@ -43,9 +43,9 @@ const server = z.object({ */ const client = z.object({ // NEXT_PUBLIC_CLIENTVAR: z.string().min(1), - NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string(), - NEXT_PUBLIC_CRISP_WEBSITE_ID: z.string(), - NEXT_PUBLIC_POSTHOG_KEY: z.string(), + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().nullish(), + NEXT_PUBLIC_CRISP_WEBSITE_ID: z.string().nullish(), + NEXT_PUBLIC_POSTHOG_KEY: z.string().nullish(), NEXT_PUBLIC_POSTHOG_HOST: z.string(), NEXT_PUBLIC_POSTHOG_PROXY_HOST: z.string(), }); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 08313b7..819e2fa 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -12,12 +12,12 @@ import { env } from "~/env.mjs"; import { type ReactNode, useEffect } from "react"; // Check that PostHog is client-side (used to handle Next.js SSR) -if (typeof window !== "undefined") { +if (typeof window !== "undefined" && !!env.NEXT_PUBLIC_POSTHOG_KEY) { posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, { api_host: env.NEXT_PUBLIC_POSTHOG_PROXY_HOST, // Enable debug mode in development loaded: (posthog) => { - if (process.env.NODE_ENV === "development") posthog.debug(false); + if (process.env.NODE_ENV === "development") posthog.debug(); }, }); } @@ -47,16 +47,16 @@ const PostHogIdentificationWrapper = ({ const posthog = usePostHog(); useEffect(() => { - if (!posthog) return; + if (!posthog?.__loaded) return; if (status === "authenticated") { const { id, name, email, stripeSubscriptionStatus } = session?.user; - posthog.identify(id, { + posthog?.identify(id, { name, email, stripeSubscriptionStatus, }); } else if (status === "unauthenticated") { - posthog.reset(); + posthog?.reset(); } }, [posthog, session, status]); diff --git a/src/pages/api/webhooks/stripe.ts b/src/pages/api/webhooks/stripe.ts index a7d4c8c..643b3cd 100644 --- a/src/pages/api/webhooks/stripe.ts +++ b/src/pages/api/webhooks/stripe.ts @@ -24,6 +24,10 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse ) { + if (!webhookSecret || !stripe) { + return res.status(500).end("Stripe env variables not set"); + } + if (req.method === "POST") { const buf = await buffer(req); const sig = req.headers["stripe-signature"]; @@ -72,11 +76,11 @@ export default async function handler( const userId = subscription.metadata.userId; if (userId) { - posthog.capture({ + posthog?.capture({ distinctId: userId, event: "stripe invoice.payment_failed", }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); } break; case "customer.subscription.deleted": diff --git a/src/server/api/routers/stripe.ts b/src/server/api/routers/stripe.ts index 65a0df9..2fce39e 100644 --- a/src/server/api/routers/stripe.ts +++ b/src/server/api/routers/stripe.ts @@ -8,6 +8,14 @@ export const stripeRouter = createTRPCRouter({ .input(z.object({ billedAnnually: z.boolean() })) .mutation( async ({ ctx: { prisma, stripe, session, req, posthog }, input }) => { + if ( + !stripe || + !env.STRIPE_ANNUAL_PRICE_ID || + !env.STRIPE_MONTHLY_PRICE_ID + ) { + throw new Error("Stripe env variables not set"); + } + const customerId = await getOrCreateStripeCustomerIdForUser({ prisma, stripe, @@ -50,20 +58,24 @@ export const stripeRouter = createTRPCRouter({ throw new Error("Could not create checkout session"); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "visiting checkout page", properties: { billingCycle: input.billedAnnually ? "annual" : "monthly", }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); return { checkoutUrl: checkoutSession.url }; } ), createBillingPortalSession: protectedProcedure.mutation( async ({ ctx: { stripe, session, prisma, req, posthog } }) => { + if (!stripe) { + throw new Error("Stripe env variables not set"); + } + const customerId = await getOrCreateStripeCustomerIdForUser({ prisma, stripe, @@ -89,14 +101,14 @@ export const stripeRouter = createTRPCRouter({ throw new Error("Could not create billing portal session"); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "visiting billing portal page", properties: { stripeSubscriptionStatus: session.user.stripeSubscriptionStatus, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); return { billingPortalUrl: stripeBillingPortalSession.url }; } diff --git a/src/server/api/routers/video.ts b/src/server/api/routers/video.ts index 913f7e2..1f2c85a 100644 --- a/src/server/api/routers/video.ts +++ b/src/server/api/routers/video.ts @@ -23,14 +23,14 @@ export const videoRouter = createTRPCRouter({ }, }); - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "viewing video list", properties: { videoAmount: videos.length, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); const videosWithThumbnailUrl = await Promise.all( videos.map(async (video) => { @@ -70,7 +70,7 @@ export const videoRouter = createTRPCRouter({ } if (session) { - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "viewing video", properties: { @@ -83,7 +83,7 @@ export const videoRouter = createTRPCRouter({ videoShareLinkExpiresAt: video.shareLinkExpiresAt, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); } const getObjectCommand = new GetObjectCommand({ @@ -110,7 +110,7 @@ export const videoRouter = createTRPCRouter({ videos.length >= 10 && session.user.stripeSubscriptionStatus !== "active" ) { - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "hit video upload limit", properties: { @@ -118,7 +118,7 @@ export const videoRouter = createTRPCRouter({ stripeSubscriptionStatus: session.user.stripeSubscriptionStatus, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); throw new TRPCError({ code: "FORBIDDEN", @@ -127,7 +127,7 @@ export const videoRouter = createTRPCRouter({ }); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "uploading video", properties: { @@ -135,7 +135,7 @@ export const videoRouter = createTRPCRouter({ stripeSubscriptionStatus: session.user.stripeSubscriptionStatus, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); const video = await prisma.video.create({ data: { @@ -184,7 +184,7 @@ export const videoRouter = createTRPCRouter({ throw new TRPCError({ code: "FORBIDDEN" }); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "update video setSharing", properties: { @@ -192,7 +192,7 @@ export const videoRouter = createTRPCRouter({ videoSharing: input.sharing, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); return { success: true, @@ -218,7 +218,7 @@ export const videoRouter = createTRPCRouter({ throw new TRPCError({ code: "FORBIDDEN" }); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "update video delete_after_link_expires", properties: { @@ -226,7 +226,7 @@ export const videoRouter = createTRPCRouter({ delete_after_link_expires: input.delete_after_link_expires, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); return { success: true, @@ -255,7 +255,7 @@ export const videoRouter = createTRPCRouter({ throw new TRPCError({ code: "FORBIDDEN" }); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "update video shareLinkExpiresAt", properties: { @@ -263,7 +263,7 @@ export const videoRouter = createTRPCRouter({ shareLinkExpiresAt: input.shareLinkExpiresAt, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); return { success: true, @@ -292,7 +292,7 @@ export const videoRouter = createTRPCRouter({ throw new TRPCError({ code: "FORBIDDEN" }); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "update video title", properties: { @@ -300,7 +300,7 @@ export const videoRouter = createTRPCRouter({ title: input.title, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); return { success: true, @@ -325,14 +325,14 @@ export const videoRouter = createTRPCRouter({ throw new TRPCError({ code: "FORBIDDEN" }); } - posthog.capture({ + posthog?.capture({ distinctId: session.user.id, event: "video delete", properties: { videoId: input.videoId, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); const deleteVideoObject = await s3.send( new DeleteObjectCommand({ diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 36506cc..60d7f2d 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -125,9 +125,11 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { if (!ctx.session || !ctx.session.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } - const { success } = await rateLimit.limit(ctx.session.user.id); - if (!success) { - throw new TRPCError({ code: "TOO_MANY_REQUESTS" }); + if (rateLimit) { + const { success } = await rateLimit.limit(ctx.session.user.id); + if (!success) { + throw new TRPCError({ code: "TOO_MANY_REQUESTS" }); + } } return next({ diff --git a/src/server/auth.ts b/src/server/auth.ts index b4e3589..41eeb68 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -70,40 +70,44 @@ export const authOptions: NextAuthOptions = { ], events: { async signIn(message) { - const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { - host: env.NEXT_PUBLIC_POSTHOG_HOST, - }); + if (!!env.NEXT_PUBLIC_POSTHOG_KEY) { + const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { + host: env.NEXT_PUBLIC_POSTHOG_HOST, + }); - client.capture({ - distinctId: message.user.id, - event: "user logged in", - properties: { - provider: message.account?.provider, - isNewUser: message.isNewUser, - }, - }); + client.capture({ + distinctId: message.user.id, + event: "user logged in", + properties: { + provider: message.account?.provider, + isNewUser: message.isNewUser, + }, + }); - await client.shutdownAsync(); + await client.shutdownAsync(); + } }, async signOut(message) { - const session = message.session as unknown as { - id: string; - sessionToken: string; - userId: string; - expires: Date; - }; - if (!session?.userId) return; + if (!!env.NEXT_PUBLIC_POSTHOG_KEY) { + const session = message.session as unknown as { + id: string; + sessionToken: string; + userId: string; + expires: Date; + }; + if (!session?.userId) return; - const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { - host: env.NEXT_PUBLIC_POSTHOG_HOST, - }); + const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { + host: env.NEXT_PUBLIC_POSTHOG_HOST, + }); - client.capture({ - distinctId: session.userId, - event: "user logged out", - }); + client.capture({ + distinctId: session.userId, + event: "user logged out", + }); - await client.shutdownAsync(); + await client.shutdownAsync(); + } }, }, pages: { diff --git a/src/server/posthog.ts b/src/server/posthog.ts index 1c9869e..88fe0c1 100644 --- a/src/server/posthog.ts +++ b/src/server/posthog.ts @@ -1,6 +1,8 @@ import { PostHog } from "posthog-node"; import { env } from "~/env.mjs"; -export const posthog = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { - host: env.NEXT_PUBLIC_POSTHOG_HOST, -}); +export const posthog = !!env.NEXT_PUBLIC_POSTHOG_KEY + ? new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { + host: env.NEXT_PUBLIC_POSTHOG_HOST, + }) + : null; diff --git a/src/server/rateLimit.ts b/src/server/rateLimit.ts index fba3dab..f9460e5 100644 --- a/src/server/rateLimit.ts +++ b/src/server/rateLimit.ts @@ -2,10 +2,13 @@ import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; import { env } from "~/env.mjs"; -export const rateLimit = new Ratelimit({ - redis: new Redis({ - url: env.UPSTASH_REDIS_REST_URL, - token: env.UPSTASH_REDIS_REST_TOKEN, - }), - limiter: Ratelimit.slidingWindow(60, "60 s"), -}); +export const rateLimit = + !!env.UPSTASH_REDIS_REST_URL && !!env.UPSTASH_REDIS_REST_TOKEN + ? new Ratelimit({ + redis: new Redis({ + url: env.UPSTASH_REDIS_REST_URL, + token: env.UPSTASH_REDIS_REST_TOKEN, + }), + limiter: Ratelimit.slidingWindow(60, "60 s"), + }) + : null; diff --git a/src/server/stripe-webhook-handlers.ts b/src/server/stripe-webhook-handlers.ts index 397adf6..f8585b0 100644 --- a/src/server/stripe-webhook-handlers.ts +++ b/src/server/stripe-webhook-handlers.ts @@ -60,7 +60,7 @@ export const handleInvoicePaid = async ({ event: Stripe.Event; stripe: Stripe; prisma: PrismaClient; - posthog: PostHog; + posthog: PostHog | null; }) => { const invoice = event.data.object as Stripe.Invoice; const subscriptionId = invoice.subscription; @@ -81,14 +81,14 @@ export const handleInvoicePaid = async ({ }); if (userId && subscription.status) { - posthog.capture({ + posthog?.capture({ distinctId: userId, event: "stripe invoice.paid", properties: { stripeSubscriptionStatus: subscription.status, }, }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); } }; @@ -99,7 +99,7 @@ export const handleSubscriptionCreatedOrUpdated = async ({ }: { event: Stripe.Event; prisma: PrismaClient; - posthog: PostHog; + posthog: PostHog | null; }) => { const subscription = event.data.object as Stripe.Subscription; const userId = subscription.metadata.userId; @@ -116,11 +116,11 @@ export const handleSubscriptionCreatedOrUpdated = async ({ }); if (userId && subscription.status) { - posthog.capture({ + posthog?.capture({ distinctId: userId, event: "stripe subscription created or updated", }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); } }; @@ -131,7 +131,7 @@ export const handleSubscriptionCanceled = async ({ }: { event: Stripe.Event; prisma: PrismaClient; - posthog: PostHog; + posthog: PostHog | null; }) => { const subscription = event.data.object as Stripe.Subscription; const userId = subscription.metadata.userId; @@ -148,10 +148,10 @@ export const handleSubscriptionCanceled = async ({ }); if (userId && subscription.status) { - posthog.capture({ + posthog?.capture({ distinctId: userId, event: "stripe subscription cancelled", }); - void posthog.shutdownAsync(); + void posthog?.shutdownAsync(); } }; diff --git a/src/server/stripe.ts b/src/server/stripe.ts index ad68bf7..f65ef2f 100644 --- a/src/server/stripe.ts +++ b/src/server/stripe.ts @@ -1,5 +1,8 @@ import Stripe from "stripe"; import { env } from "~/env.mjs"; -export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { - apiVersion: "2022-11-15", -}); + +export const stripe = !!env.STRIPE_SECRET_KEY + ? new Stripe(env.STRIPE_SECRET_KEY, { + apiVersion: "2022-11-15", + }) + : null;