diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..f3006cb25 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +target/ +node_modules/ +crates/ diff --git a/.npmrc b/.npmrc index 1560b99cc..bb7b7ccd8 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -auto-install-peers = true \ No newline at end of file +auto-install-peers = true +inject-workspace-packages=true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c04b6071c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# syntax=docker.io/docker/dockerfile:1 + +FROM node:20-alpine AS base +RUN corepack enable + +FROM base AS builder +WORKDIR /app +COPY . . + +RUN corepack enable pnpm +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm i --frozen-lockfile + +ARG DOCKER_BUILD=true +ENV NEXT_PUBLIC_WEB_URL=http://localhost:3000 +ENV NEXT_PUBLIC_CAP_AWS_BUCKET=capso +ENV NEXT_PUBLIC_CAP_AWS_REGION=us-east-1 + +RUN pnpm run build:web + + +# 3. Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nextjs -u 1001 + + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public +COPY --from=builder --chown=nextjs:nodejs /app/packages/database/migrations ./apps/web/migrations + + +USER nextjs + +EXPOSE 3000 + +CMD HOSTNAME="0.0.0.0" node apps/web/server.js diff --git a/apps/web/actions/screenshots/get-screenshot.ts b/apps/web/actions/screenshots/get-screenshot.ts index f9836b0d6..ee8ddd976 100644 --- a/apps/web/actions/screenshots/get-screenshot.ts +++ b/apps/web/actions/screenshots/get-screenshot.ts @@ -10,14 +10,14 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { createS3Client, getS3Bucket } from "@/utils/s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { S3_BUCKET_URL } from "@cap/utils"; -import { clientEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; export async function getScreenshot(userId: string, screenshotId: string) { if (!userId || !screenshotId) { throw new Error("userId or screenshotId not supplied"); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) @@ -65,7 +65,7 @@ export async function getScreenshot(userId: string, screenshotId: string) { let screenshotUrl: string; - if (video.awsBucket !== clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET) { + if (video.awsBucket !== serverEnv().CAP_AWS_BUCKET) { screenshotUrl = await getSignedUrl( s3Client, new GetObjectCommand({ @@ -82,4 +82,4 @@ export async function getScreenshot(userId: string, screenshotId: string) { } catch (error) { throw new Error(`Error generating screenshot URL: ${error}`); } -} \ No newline at end of file +} diff --git a/apps/web/actions/videos/delete.ts b/apps/web/actions/videos/delete.ts index 7c77904fe..2ee8d27f1 100644 --- a/apps/web/actions/videos/delete.ts +++ b/apps/web/actions/videos/delete.ts @@ -4,10 +4,7 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { s3Buckets, videos } from "@cap/database/schema"; import { db } from "@cap/database"; import { and, eq } from "drizzle-orm"; -import { - DeleteObjectsCommand, - ListObjectsV2Command, -} from "@aws-sdk/client-s3"; +import { DeleteObjectsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3"; import { createS3Client, getS3Bucket } from "@/utils/s3"; export async function deleteVideo(videoId: string) { @@ -22,7 +19,7 @@ export async function deleteVideo(videoId: string) { }; } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) @@ -43,7 +40,7 @@ export async function deleteVideo(videoId: string) { }; } - await db + await db() .delete(videos) .where(and(eq(videos.id, videoId), eq(videos.ownerId, userId))); @@ -82,4 +79,4 @@ export async function deleteVideo(videoId: string) { message: "Failed to delete video", }; } -} \ No newline at end of file +} diff --git a/apps/web/actions/videos/edit-date.ts b/apps/web/actions/videos/edit-date.ts index 6e3b50caf..4a6d508b4 100644 --- a/apps/web/actions/videos/edit-date.ts +++ b/apps/web/actions/videos/edit-date.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { videos } from "@cap/database/schema"; @@ -9,13 +9,13 @@ import { revalidatePath } from "next/cache"; export async function editDate(videoId: string, date: string) { const user = await getCurrentUser(); - + if (!user || !date || !videoId) { throw new Error("Missing required data for updating video date"); } const userId = user.id; - const query = await db.select().from(videos).where(eq(videos.id, videoId)); + const query = await db().select().from(videos).where(eq(videos.id, videoId)); if (query.length === 0) { throw new Error("Video not found"); @@ -33,20 +33,20 @@ export async function editDate(videoId: string, date: string) { try { const newDate = new Date(date); const currentDate = new Date(); - + // Prevent future dates if (newDate > currentDate) { throw new Error("Cannot set a date in the future"); } - + // Store the custom date in the metadata field - const currentMetadata = video.metadata as VideoMetadata || {}; + const currentMetadata = (video.metadata as VideoMetadata) || {}; const updatedMetadata: VideoMetadata = { ...currentMetadata, customCreatedAt: newDate.toISOString(), }; - - await db + + await db() .update(videos) .set({ metadata: updatedMetadata, @@ -54,9 +54,9 @@ export async function editDate(videoId: string, date: string) { .where(eq(videos.id, videoId)); // Revalidate paths to update the UI - revalidatePath('/dashboard/caps'); - revalidatePath('/dashboard/shared-caps'); - + revalidatePath("/dashboard/caps"); + revalidatePath("/dashboard/shared-caps"); + return { success: true }; } catch (error) { console.error("Error updating video date:", error); @@ -65,4 +65,4 @@ export async function editDate(videoId: string, date: string) { } throw new Error("Failed to update video date"); } -} \ No newline at end of file +} diff --git a/apps/web/actions/videos/edit-title.ts b/apps/web/actions/videos/edit-title.ts index 2190cb007..727a7db77 100644 --- a/apps/web/actions/videos/edit-title.ts +++ b/apps/web/actions/videos/edit-title.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { videos } from "@cap/database/schema"; @@ -8,13 +8,13 @@ import { revalidatePath } from "next/cache"; export async function editTitle(videoId: string, title: string) { const user = await getCurrentUser(); - + if (!user || !title || !videoId) { throw new Error("Missing required data for updating video title"); } const userId = user.id; - const query = await db.select().from(videos).where(eq(videos.id, videoId)); + const query = await db().select().from(videos).where(eq(videos.id, videoId)); if (query.length === 0) { throw new Error("Video not found"); @@ -30,15 +30,15 @@ export async function editTitle(videoId: string, title: string) { } try { - await db + await db() .update(videos) .set({ name: title }) .where(eq(videos.id, videoId)); - revalidatePath('/dashboard/caps'); - revalidatePath('/dashboard/shared-caps'); + revalidatePath("/dashboard/caps"); + revalidatePath("/dashboard/shared-caps"); revalidatePath(`/s/${videoId}`); - + return { success: true }; } catch (error) { console.error("Error updating video title:", error); @@ -47,4 +47,4 @@ export async function editTitle(videoId: string, title: string) { } throw new Error("Failed to update video title"); } -} \ No newline at end of file +} diff --git a/apps/web/actions/videos/get-analytics.ts b/apps/web/actions/videos/get-analytics.ts index 4b699497d..4f2b76a5c 100644 --- a/apps/web/actions/videos/get-analytics.ts +++ b/apps/web/actions/videos/get-analytics.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { dub } from "@/utils/dub"; import { ClicksCount } from "dub/models/components"; @@ -9,7 +9,7 @@ export async function getVideoAnalytics(videoId: string) { } try { - const response = await dub.analytics.retrieve({ + const response = await dub().analytics.retrieve({ domain: "cap.link", key: videoId, }); @@ -28,4 +28,4 @@ export async function getVideoAnalytics(videoId: string) { console.error("Error fetching video analytics:", error); return { count: 0 }; } -} \ No newline at end of file +} diff --git a/apps/web/actions/videos/get-og-image.tsx b/apps/web/actions/videos/get-og-image.tsx index 5f6a339d1..cfae27374 100644 --- a/apps/web/actions/videos/get-og-image.tsx +++ b/apps/web/actions/videos/get-og-image.tsx @@ -2,7 +2,7 @@ import { ImageResponse } from "next/og"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { createS3Client, getS3Bucket } from "@/utils/s3"; -import { clientEnv, serverEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; import { db } from "@cap/database"; import { s3Buckets, videos } from "@cap/database/schema"; import { eq } from "drizzle-orm"; @@ -160,7 +160,7 @@ export async function generateVideoOgImage(videoId: string) { } async function getData(videoId: string) { - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, @@ -174,10 +174,10 @@ async function getData(videoId: string) { if (!result) return; const defaultBucket = { - name: clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET, - region: clientEnv.NEXT_PUBLIC_CAP_AWS_REGION, - accessKeyId: serverEnv.CAP_AWS_ACCESS_KEY, - secretAccessKey: serverEnv.CAP_AWS_SECRET_KEY, + name: serverEnv().CAP_AWS_BUCKET, + region: serverEnv().CAP_AWS_REGION, + accessKeyId: serverEnv().CAP_AWS_ACCESS_KEY, + secretAccessKey: serverEnv().CAP_AWS_SECRET_KEY, }; return { diff --git a/apps/web/actions/videos/transcribe.ts b/apps/web/actions/videos/transcribe.ts index 5e6b57302..de7145197 100644 --- a/apps/web/actions/videos/transcribe.ts +++ b/apps/web/actions/videos/transcribe.ts @@ -1,9 +1,4 @@ -"use server"; - -import { - GetObjectCommand, - PutObjectCommand, -} from "@aws-sdk/client-s3"; +import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { createClient } from "@deepgram/sdk"; import { db } from "@cap/database"; @@ -21,7 +16,7 @@ export async function transcribeVideo( videoId: string, userId: string ): Promise { - if (!serverEnv.DEEPGRAM_API_KEY) { + if (!serverEnv().DEEPGRAM_API_KEY) { return { success: false, message: "Missing necessary environment variables", @@ -35,7 +30,7 @@ export async function transcribeVideo( }; } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, @@ -79,7 +74,7 @@ export async function transcribeVideo( }; } - await db + await db() .update(videos) .set({ transcriptionStatus: "PROCESSING" }) .where(eq(videos.id, videoId)); @@ -112,7 +107,7 @@ export async function transcribeVideo( await s3Client.send(uploadCommand); - await db + await db() .update(videos) .set({ transcriptionStatus: "COMPLETE" }) .where(eq(videos.id, videoId)); @@ -122,7 +117,7 @@ export async function transcribeVideo( message: "VTT file generated and uploaded successfully", }; } catch (error) { - await db + await db() .update(videos) .set({ transcriptionStatus: "ERROR" }) .where(eq(videos.id, videoId)); @@ -179,7 +174,7 @@ function formatTimestamp(seconds: number): string { } async function transcribeAudio(videoUrl: string): Promise { - const deepgram = createClient(serverEnv.DEEPGRAM_API_KEY as string); + const deepgram = createClient(serverEnv().DEEPGRAM_API_KEY as string); const { result, error } = await deepgram.listen.prerecorded.transcribeUrl( { @@ -201,4 +196,4 @@ async function transcribeAudio(videoUrl: string): Promise { const captions = formatToWebVTT(result); return captions; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/check-domain.ts b/apps/web/actions/workspace/check-domain.ts index 8fdb64b13..b8acc4ca8 100644 --- a/apps/web/actions/workspace/check-domain.ts +++ b/apps/web/actions/workspace/check-domain.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaces } from "@cap/database/schema"; @@ -13,7 +13,10 @@ export async function checkWorkspaceDomain(spaceId: string) { throw new Error("Unauthorized"); } - const [space] = await db.select().from(spaces).where(eq(spaces.id, spaceId)); + const [space] = await db() + .select() + .from(spaces) + .where(eq(spaces.id, spaceId)); if (!space || space.ownerId !== user.id) { throw new Error("Only the owner can check domain status"); @@ -27,14 +30,14 @@ export async function checkWorkspaceDomain(spaceId: string) { const status = await checkDomainStatus(space.customDomain); if (status.verified && !space.domainVerified) { - await db + await db() .update(spaces) .set({ domainVerified: new Date(), }) .where(eq(spaces.id, spaceId)); } else if (!status.verified && space.domainVerified) { - await db + await db() .update(spaces) .set({ domainVerified: null, @@ -49,4 +52,4 @@ export async function checkWorkspaceDomain(spaceId: string) { } throw new Error("Failed to check domain status"); } -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/get-space.ts b/apps/web/actions/workspace/get-space.ts index 8ea9b235e..32f5b2fb0 100644 --- a/apps/web/actions/workspace/get-space.ts +++ b/apps/web/actions/workspace/get-space.ts @@ -9,7 +9,7 @@ export async function getSpace(spaceId: string) { throw new Error("Space ID is required"); } - const [space] = await db + const [space] = await db() .select({ workosOrganizationId: spaces.workosOrganizationId, workosConnectionId: spaces.workosConnectionId, @@ -27,4 +27,4 @@ export async function getSpace(spaceId: string) { connectionId: space.workosConnectionId, name: space.name, }; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/manage-billing.ts b/apps/web/actions/workspace/manage-billing.ts index 6f91cb77d..c93a8f238 100644 --- a/apps/web/actions/workspace/manage-billing.ts +++ b/apps/web/actions/workspace/manage-billing.ts @@ -1,11 +1,11 @@ -'use server'; +"use server"; import { stripe } from "@cap/utils"; import { getCurrentUser } from "@cap/database/auth/session"; import { eq } from "drizzle-orm"; import { db } from "@cap/database"; import { users } from "@cap/database/schema"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; export async function manageBilling() { const user = await getCurrentUser(); @@ -16,14 +16,14 @@ export async function manageBilling() { } if (!user.stripeCustomerId) { - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id, }, }); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id, @@ -33,10 +33,10 @@ export async function manageBilling() { customerId = customer.id; } - const { url } = await stripe.billingPortal.sessions.create({ + const { url } = await stripe().billingPortal.sessions.create({ customer: customerId as string, - return_url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/dashboard/settings/workspace`, + return_url: `${serverEnv().WEB_URL}/dashboard/settings/workspace`, }); - + return url; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/remove-domain.ts b/apps/web/actions/workspace/remove-domain.ts index ec8196905..a7de90654 100644 --- a/apps/web/actions/workspace/remove-domain.ts +++ b/apps/web/actions/workspace/remove-domain.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaces } from "@cap/database/schema"; @@ -13,7 +13,10 @@ export async function removeWorkspaceDomain(spaceId: string) { throw new Error("Unauthorized"); } - const [space] = await db.select().from(spaces).where(eq(spaces.id, spaceId)); + const [space] = await db() + .select() + .from(spaces) + .where(eq(spaces.id, spaceId)); if (!space || space.ownerId !== user.id) { throw new Error("Only the owner can remove the custom domain"); @@ -36,7 +39,7 @@ export async function removeWorkspaceDomain(spaceId: string) { ); } - await db + await db() .update(spaces) .set({ customDomain: null, @@ -44,8 +47,8 @@ export async function removeWorkspaceDomain(spaceId: string) { }) .where(eq(spaces.id, spaceId)); - revalidatePath('/dashboard/settings/workspace'); - + revalidatePath("/dashboard/settings/workspace"); + return { success: true }; } catch (error) { if (error instanceof Error) { @@ -53,4 +56,4 @@ export async function removeWorkspaceDomain(spaceId: string) { } throw new Error("Failed to remove domain"); } -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/remove-invite.ts b/apps/web/actions/workspace/remove-invite.ts index 58d0e2f95..6e04c2ab5 100644 --- a/apps/web/actions/workspace/remove-invite.ts +++ b/apps/web/actions/workspace/remove-invite.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaces, spaceInvites } from "@cap/database/schema"; @@ -6,17 +6,14 @@ import { db } from "@cap/database"; import { eq, and } from "drizzle-orm"; import { revalidatePath } from "next/cache"; -export async function removeWorkspaceInvite( - inviteId: string, - spaceId: string -) { +export async function removeWorkspaceInvite(inviteId: string, spaceId: string) { const user = await getCurrentUser(); if (!user) { throw new Error("Unauthorized"); } - const space = await db + const space = await db() .select() .from(spaces) .where(eq(spaces.id, spaceId)) @@ -30,7 +27,7 @@ export async function removeWorkspaceInvite( throw new Error("Only the owner can remove workspace invites"); } - const result = await db + const result = await db() .delete(spaceInvites) .where( and(eq(spaceInvites.id, inviteId), eq(spaceInvites.spaceId, spaceId)) @@ -40,7 +37,7 @@ export async function removeWorkspaceInvite( throw new Error("Invite not found"); } - revalidatePath('/dashboard/settings/workspace'); - + revalidatePath("/dashboard/settings/workspace"); + return { success: true }; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/send-invites.ts b/apps/web/actions/workspace/send-invites.ts index 5ce774527..d8fc1ec3e 100644 --- a/apps/web/actions/workspace/send-invites.ts +++ b/apps/web/actions/workspace/send-invites.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaces, spaceInvites } from "@cap/database/schema"; @@ -7,7 +7,7 @@ import { eq } from "drizzle-orm"; import { nanoId } from "@cap/database/helpers"; import { sendEmail } from "@cap/database/emails/config"; import { WorkspaceInvite } from "@cap/database/emails/workspace-invite"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; import { revalidatePath } from "next/cache"; export async function sendWorkspaceInvites( @@ -20,7 +20,7 @@ export async function sendWorkspaceInvites( throw new Error("Unauthorized"); } - const space = await db.select().from(spaces).where(eq(spaces.id, spaceId)); + const space = await db().select().from(spaces).where(eq(spaces.id, spaceId)); if (!space || space.length === 0) { throw new Error("Workspace not found"); @@ -37,7 +37,7 @@ export async function sendWorkspaceInvites( for (const email of validEmails) { const inviteId = nanoId(); - await db.insert(spaceInvites).values({ + await db().insert(spaceInvites).values({ id: inviteId, spaceId: spaceId, invitedEmail: email.trim(), @@ -46,7 +46,7 @@ export async function sendWorkspaceInvites( }); // Send invitation email - const inviteUrl = `${clientEnv.NEXT_PUBLIC_WEB_URL}/invite/${inviteId}`; + const inviteUrl = `${serverEnv().WEB_URL}/invite/${inviteId}`; await sendEmail({ email: email.trim(), subject: `Invitation to join ${space[0].name} on Cap`, @@ -58,7 +58,7 @@ export async function sendWorkspaceInvites( }); } - revalidatePath('/dashboard/settings/workspace'); - + revalidatePath("/dashboard/settings/workspace"); + return { success: true }; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/update-details.ts b/apps/web/actions/workspace/update-details.ts index 20632818b..513cab465 100644 --- a/apps/web/actions/workspace/update-details.ts +++ b/apps/web/actions/workspace/update-details.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaces } from "@cap/database/schema"; @@ -17,7 +17,7 @@ export async function updateWorkspaceDetails( throw new Error("Unauthorized"); } - const space = await db.select().from(spaces).where(eq(spaces.id, spaceId)); + const space = await db().select().from(spaces).where(eq(spaces.id, spaceId)); if (!space || space.length === 0) { throw new Error("Workspace not found"); @@ -27,7 +27,7 @@ export async function updateWorkspaceDetails( throw new Error("Only the owner can update workspace details"); } - await db + await db() .update(spaces) .set({ name: workspaceName, @@ -35,7 +35,7 @@ export async function updateWorkspaceDetails( }) .where(eq(spaces.id, spaceId)); - revalidatePath('/dashboard/settings/workspace'); - + revalidatePath("/dashboard/settings/workspace"); + return { success: true }; -} \ No newline at end of file +} diff --git a/apps/web/actions/workspace/update-domain.ts b/apps/web/actions/workspace/update-domain.ts index c648deadc..664c865ec 100644 --- a/apps/web/actions/workspace/update-domain.ts +++ b/apps/web/actions/workspace/update-domain.ts @@ -1,4 +1,4 @@ -'use server'; +"use server"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaces } from "@cap/database/schema"; @@ -14,7 +14,10 @@ export async function updateDomain(domain: string, spaceId: string) { throw new Error("Unauthorized"); } - const [space] = await db.select().from(spaces).where(eq(spaces.id, spaceId)); + const [space] = await db() + .select() + .from(spaces) + .where(eq(spaces.id, spaceId)); if (!space || space.ownerId !== user.id) { throw new Error("Only the owner can update the custom domain"); @@ -27,7 +30,7 @@ export async function updateDomain(domain: string, spaceId: string) { throw new Error(addDomainResponse.error.message); } - await db + await db() .update(spaces) .set({ customDomain: domain, @@ -38,7 +41,7 @@ export async function updateDomain(domain: string, spaceId: string) { const status = await checkDomainStatus(domain); if (status.verified) { - await db + await db() .update(spaces) .set({ domainVerified: new Date(), @@ -46,8 +49,8 @@ export async function updateDomain(domain: string, spaceId: string) { .where(eq(spaces.id, spaceId)); } - revalidatePath('/dashboard/settings/workspace'); - + revalidatePath("/dashboard/settings/workspace"); + return status; } catch (error) { if (error instanceof Error) { @@ -55,4 +58,4 @@ export async function updateDomain(domain: string, spaceId: string) { } throw new Error("Failed to update domain"); } -} \ No newline at end of file +} diff --git a/apps/web/app/AuthProvider.tsx b/apps/web/app/AuthProvider.tsx index bb80a6258..f22904e22 100644 --- a/apps/web/app/AuthProvider.tsx +++ b/apps/web/app/AuthProvider.tsx @@ -1,7 +1,8 @@ "use client"; import { SessionProvider } from "next-auth/react"; +import { PropsWithChildren } from "react"; -export function AuthProvider({ children }: { children: React.ReactNode }) { +export function AuthProvider({ children }: PropsWithChildren) { return {children}; } diff --git a/apps/web/app/[slug]/layout.tsx b/apps/web/app/[slug]/layout.tsx index a14e64fcd..5ff842ef0 100644 --- a/apps/web/app/[slug]/layout.tsx +++ b/apps/web/app/[slug]/layout.tsx @@ -1,16 +1,16 @@ export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} + title: "Next.js", + description: "Generated by Next.js", +}; export default function RootLayout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { return ( {children} - ) + ); } diff --git a/apps/web/app/api/auth/[...nextauth]/route.ts b/apps/web/app/api/auth/[...nextauth]/route.ts index 565ccb6cb..07032156a 100644 --- a/apps/web/app/api/auth/[...nextauth]/route.ts +++ b/apps/web/app/api/auth/[...nextauth]/route.ts @@ -1,6 +1,8 @@ import NextAuth from "next-auth"; import { authOptions } from "@cap/database/auth/auth-options"; -const handler = NextAuth(authOptions); +export const dynamic = "force-dynamic"; + +const handler = NextAuth(authOptions()); export { handler as GET, handler as POST }; diff --git a/apps/web/app/api/caps/share/route.ts b/apps/web/app/api/caps/share/route.ts index e596bbf66..b53c33bec 100644 --- a/apps/web/app/api/caps/share/route.ts +++ b/apps/web/app/api/caps/share/route.ts @@ -15,13 +15,13 @@ export async function POST(request: NextRequest) { try { // Check if the user owns the cap - const [cap] = await db.select().from(videos).where(eq(videos.id, capId)); + const [cap] = await db().select().from(videos).where(eq(videos.id, capId)); if (!cap || cap.ownerId !== user.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } // Get current shared spaces - const currentSharedSpaces = await db + const currentSharedSpaces = await db() .select() .from(sharedVideos) .where(eq(sharedVideos.videoId, capId)); @@ -29,7 +29,7 @@ export async function POST(request: NextRequest) { // Remove spaces that are no longer shared for (const sharedSpace of currentSharedSpaces) { if (!spaceIds.includes(sharedSpace.spaceId)) { - await db + await db() .delete(sharedVideos) .where( and( @@ -46,7 +46,7 @@ export async function POST(request: NextRequest) { (share) => share.spaceId === spaceId ); if (!existingShare) { - await db.insert(sharedVideos).values({ + await db().insert(sharedVideos).values({ id: nanoId(), videoId: capId, spaceId: spaceId, diff --git a/apps/web/app/api/changelog/status/route.ts b/apps/web/app/api/changelog/status/route.ts index 339683bf8..d5a84f4ee 100644 --- a/apps/web/app/api/changelog/status/route.ts +++ b/apps/web/app/api/changelog/status/route.ts @@ -1,7 +1,6 @@ import { NextResponse } from "next/server"; import { getChangelogPosts } from "../../../../utils/changelog"; -export const dynamic = "force-dynamic"; export const revalidate = 0; export async function GET(request: Request) { diff --git a/apps/web/app/api/desktop/app.ts b/apps/web/app/api/desktop/app.ts index 4ab2f67b2..cf06101c3 100644 --- a/apps/web/app/api/desktop/app.ts +++ b/apps/web/app/api/desktop/app.ts @@ -1,4 +1,4 @@ -import { clientEnv, serverEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; @@ -19,7 +19,7 @@ app.post( try { // Send feedback to Discord channel - const discordWebhookUrl = serverEnv.DISCORD_FEEDBACK_WEBHOOK_URL; + const discordWebhookUrl = serverEnv().DISCORD_FEEDBACK_WEBHOOK_URL; if (!discordWebhookUrl) throw new Error("Discord webhook URL is not configured"); @@ -60,7 +60,7 @@ app.get("/plan", async (c) => { if (!isSubscribed && !user.stripeSubscriptionId && user.stripeCustomerId) { try { - const subscriptions = await stripe.subscriptions.list({ + const subscriptions = await stripe().subscriptions.list({ customer: user.stripeCustomerId, }); const activeSubscription = subscriptions.data.find( @@ -68,7 +68,7 @@ app.get("/plan", async (c) => { ); if (activeSubscription) { isSubscribed = true; - await db + await db() .update(users) .set({ stripeSubscriptionStatus: activeSubscription.status, @@ -82,9 +82,10 @@ app.get("/plan", async (c) => { } let intercomHash = ""; - if (serverEnv.INTERCOM_SECRET) { + const intercomSecret = serverEnv().INTERCOM_SECRET; + if (intercomSecret) { intercomHash = crypto - .createHmac("sha256", serverEnv.INTERCOM_SECRET) + .createHmac("sha256", intercomSecret) .update(user?.id ?? "") .digest("hex"); } @@ -114,12 +115,12 @@ app.post( if (user.stripeCustomerId === null) { console.log("[POST] Creating new Stripe customer"); - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id }, }); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id }) .where(eq(users.id, user.id)); @@ -129,12 +130,12 @@ app.post( } console.log("[POST] Creating checkout session"); - const checkoutSession = await stripe.checkout.sessions.create({ + const checkoutSession = await stripe().checkout.sessions.create({ customer: customerId as string, line_items: [{ price: priceId, quantity: 1 }], mode: "subscription", - success_url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/dashboard/caps?upgrade=true`, - cancel_url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/pricing`, + success_url: `${serverEnv().WEB_URL}/dashboard/caps?upgrade=true`, + cancel_url: `${serverEnv().WEB_URL}/pricing`, allow_promotion_codes: true, }); diff --git a/apps/web/app/api/desktop/s3/config/app.ts b/apps/web/app/api/desktop/s3/config/app.ts index 4d00abc94..67af6a2cc 100644 --- a/apps/web/app/api/desktop/s3/config/app.ts +++ b/apps/web/app/api/desktop/s3/config/app.ts @@ -42,20 +42,20 @@ app.post( }; // Check if user already has a bucket config - const [existingBucket] = await db + const [existingBucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); if (existingBucket) { // Update existing config - await db + await db() .update(s3Buckets) .set(encryptedConfig) .where(eq(s3Buckets.id, existingBucket.id)); } else { // Insert new config - await db.insert(s3Buckets).values(encryptedConfig); + await db().insert(s3Buckets).values(encryptedConfig); } return c.json({ success: true }); @@ -77,7 +77,7 @@ app.delete("/delete", async (c) => { try { // Delete the S3 configuration for the user - await db.delete(s3Buckets).where(eq(s3Buckets.ownerId, user.id)); + await db().delete(s3Buckets).where(eq(s3Buckets.ownerId, user.id)); return c.json({ success: true }); } catch (error) { @@ -96,7 +96,7 @@ app.get("/get", async (c) => { const user = c.get("user"); try { - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); diff --git a/apps/web/app/api/desktop/session/app.ts b/apps/web/app/api/desktop/session/app.ts index e733e97f2..800cada4a 100644 --- a/apps/web/app/api/desktop/session/app.ts +++ b/apps/web/app/api/desktop/session/app.ts @@ -1,6 +1,6 @@ import { authOptions } from "@cap/database/auth/auth-options"; import { getCurrentUser } from "@cap/database/auth/session"; -import { clientEnv, serverEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { getCookie } from "hono/cookie"; @@ -24,12 +24,14 @@ app.get( async (c) => { const { port, platform } = c.req.valid("query"); - const secret = serverEnv.NEXTAUTH_SECRET; + const secret = serverEnv().NEXTAUTH_SECRET; const url = new URL(c.req.url); - const loginRedirectUrl = `${clientEnv.NEXT_PUBLIC_WEB_URL}/login?next=${clientEnv.NEXT_PUBLIC_WEB_URL}${url.pathname}${url.search}`; + const loginRedirectUrl = `${serverEnv().WEB_URL}/login?next=${ + serverEnv().WEB_URL + }${url.pathname}${url.search}`; - const session = await getServerSession(authOptions); + const session = await getServerSession(authOptions()); if (!session) return c.redirect(loginRedirectUrl); const token = getCookie(c, "next-auth.session-token"); diff --git a/apps/web/app/api/desktop/video/app.ts b/apps/web/app/api/desktop/video/app.ts index 4b2aa4ecd..70748525f 100644 --- a/apps/web/app/api/desktop/video/app.ts +++ b/apps/web/app/api/desktop/video/app.ts @@ -5,7 +5,7 @@ import { sendEmail } from "@cap/database/emails/config"; import { FirstShareableLink } from "@cap/database/emails/first-shareable-link"; import { nanoId } from "@cap/database/helpers"; import { s3Buckets, videos } from "@cap/database/schema"; -import { clientEnv, NODE_ENV } from "@cap/env"; +import { buildEnv, NODE_ENV, serverEnv } from "@cap/env"; import { zValidator } from "@hono/zod-validator"; import { count, eq } from "drizzle-orm"; import { Hono } from "hono"; @@ -39,7 +39,7 @@ app.get( if (!isUpgraded && duration && duration > 300) return c.json({ error: "upgrade_required" }, { status: 403 }); - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); @@ -53,7 +53,7 @@ app.get( })} ${date.getFullYear()}`; if (videoId !== undefined) { - const [video] = await db + const [video] = await db() .select() .from(videos) .where(eq(videos.id, videoId)); @@ -88,18 +88,18 @@ app.get( bucket: bucket?.id, }; - await db.insert(videos).values(videoData); + await db().insert(videos).values(videoData); - if (clientEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production") - await dub.links.create({ - url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/s/${idToUse}`, + if (buildEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production") + await dub().links.create({ + url: `${serverEnv().WEB_URL}/s/${idToUse}`, domain: "cap.link", key: idToUse, }); // Check if this is the user's first video and send the first shareable link email try { - const videoCount = await db + const videoCount = await db() .select({ count: count() }) .from(videos) .where(eq(videos.ownerId, user.id)); @@ -114,9 +114,9 @@ app.get( "[SendFirstShareableLinkEmail] Sending first shareable link email with 5-minute delay" ); - const videoUrl = clientEnv.NEXT_PUBLIC_IS_CAP + const videoUrl = buildEnv.NEXT_PUBLIC_IS_CAP ? `https://cap.link/${idToUse}` - : `${clientEnv.NEXT_PUBLIC_WEB_URL}/s/${idToUse}`; + : `${serverEnv().WEB_URL}/s/${idToUse}`; // Send email with 5-minute delay using Resend's scheduling feature await sendEmail({ diff --git a/apps/web/app/api/email/new-comment/route.ts b/apps/web/app/api/email/new-comment/route.ts index 0e12f8e73..134e8ddfb 100644 --- a/apps/web/app/api/email/new-comment/route.ts +++ b/apps/web/app/api/email/new-comment/route.ts @@ -5,7 +5,7 @@ import { db } from "@cap/database"; import { eq, and, gt, ne } from "drizzle-orm"; import { sendEmail } from "@cap/database/emails/config"; import { NewComment } from "@cap/database/emails/new-comment"; -import { clientEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; // Cache to store the last email sent time for each user const lastEmailSentCache = new Map(); @@ -25,7 +25,7 @@ export async function POST(request: NextRequest) { try { console.log(`Fetching comment details for commentId: ${commentId}`); // Get the comment details - const commentDetails = await db + const commentDetails = await db() .select({ id: comments.id, content: comments.content, @@ -68,7 +68,7 @@ export async function POST(request: NextRequest) { console.log(`Fetching video details for videoId: ${comment.videoId}`); // Get the video details - const videoDetails = await db + const videoDetails = await db() .select({ id: videos.id, name: videos.name, @@ -97,7 +97,7 @@ export async function POST(request: NextRequest) { console.log(`Fetching owner details for userId: ${video.ownerId}`); // Get the video owner's email - const ownerDetails = await db + const ownerDetails = await db() .select({ id: users.id, email: users.email, @@ -128,7 +128,7 @@ export async function POST(request: NextRequest) { let commenterName = "Anonymous"; if (comment.authorId) { console.log(`Fetching commenter details for userId: ${comment.authorId}`); - const commenterDetails = await db + const commenterDetails = await db() .select({ id: users.id, name: users.name, @@ -178,7 +178,7 @@ export async function POST(request: NextRequest) { console.log( `Checking for recent comments since ${fifteenMinutesAgo.toISOString()}` ); - const recentComments = await db + const recentComments = await db() .select({ id: comments.id, }) @@ -205,9 +205,9 @@ export async function POST(request: NextRequest) { } // Generate the video URL - const videoUrl = clientEnv.NEXT_PUBLIC_IS_CAP + const videoUrl = buildEnv.NEXT_PUBLIC_IS_CAP ? `https://cap.link/${video.id}` - : `${clientEnv.NEXT_PUBLIC_WEB_URL}/s/${video.id}`; + : `${serverEnv().WEB_URL}/s/${video.id}`; console.log(`Generated video URL: ${videoUrl}`); // Send the email diff --git a/apps/web/app/api/invite/accept/route.ts b/apps/web/app/api/invite/accept/route.ts index 404f32dce..d3e2e7170 100644 --- a/apps/web/app/api/invite/accept/route.ts +++ b/apps/web/app/api/invite/accept/route.ts @@ -15,7 +15,7 @@ export async function POST(request: NextRequest) { try { // Find the invite - const [invite] = await db + const [invite] = await db() .select() .from(spaceInvites) .where(eq(spaceInvites.id, inviteId)); @@ -30,7 +30,7 @@ export async function POST(request: NextRequest) { } // Get the space owner's subscription ID - const [spaceOwner] = await db + const [spaceOwner] = await db() .select({ stripeSubscriptionId: users.stripeSubscriptionId, }) @@ -45,7 +45,7 @@ export async function POST(request: NextRequest) { } // Create a new space member - await db.insert(spaceMembers).values({ + await db().insert(spaceMembers).values({ id: nanoId(), spaceId: invite.spaceId, userId: user.id, @@ -53,7 +53,7 @@ export async function POST(request: NextRequest) { }); // Update the user's thirdPartyStripeSubscriptionId - await db + await db() .update(users) .set({ thirdPartyStripeSubscriptionId: spaceOwner.stripeSubscriptionId, @@ -61,7 +61,7 @@ export async function POST(request: NextRequest) { .where(eq(users.id, user.id)); // Delete the invite - await db.delete(spaceInvites).where(eq(spaceInvites.id, inviteId)); + await db().delete(spaceInvites).where(eq(spaceInvites.id, inviteId)); return NextResponse.json({ success: true }); } catch (error) { diff --git a/apps/web/app/api/invite/decline/route.ts b/apps/web/app/api/invite/decline/route.ts index b0b995b64..26c5caa22 100644 --- a/apps/web/app/api/invite/decline/route.ts +++ b/apps/web/app/api/invite/decline/route.ts @@ -8,7 +8,7 @@ export async function POST(request: NextRequest) { try { // Delete the invite - await db.delete(spaceInvites).where(eq(spaceInvites.id, inviteId)); + await db().delete(spaceInvites).where(eq(spaceInvites.id, inviteId)); return NextResponse.json({ success: true }); } catch (error) { diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index 6ed96cb94..09b75e7a7 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -16,7 +16,7 @@ import { import { getHeaders, CACHE_CONTROL_HEADERS } from "@/utils/helpers"; import { createS3Client, getS3Bucket } from "@/utils/s3"; import { S3_BUCKET_URL } from "@cap/utils"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; export const revalidate = 3599; @@ -48,7 +48,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) @@ -83,7 +83,7 @@ export async function GET(request: NextRequest) { const Bucket = await getS3Bucket(bucket); const [s3Client] = await createS3Client(bucket); - if (!bucket || video.awsBucket === clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET) { + if (!bucket || video.awsBucket === serverEnv().CAP_AWS_BUCKET) { if (video.source.type === "desktopMP4") { return new Response(null, { status: 302, @@ -283,9 +283,13 @@ export async function GET(request: NextRequest) { const generatedPlaylist = await generateMasterPlaylist( videoMetadata?.Metadata?.resolution ?? "", videoMetadata?.Metadata?.bandwidth ?? "", - `${clientEnv.NEXT_PUBLIC_WEB_URL}/api/playlist?userId=${userId}&videoId=${videoId}&videoType=video`, + `${ + serverEnv().WEB_URL + }/api/playlist?userId=${userId}&videoId=${videoId}&videoType=video`, audioMetadata - ? `${clientEnv.NEXT_PUBLIC_WEB_URL}/api/playlist?userId=${userId}&videoId=${videoId}&videoType=audio` + ? `${ + serverEnv().WEB_URL + }/api/playlist?userId=${userId}&videoId=${videoId}&videoType=audio` : null, video.xStreamInfo ?? "" ); diff --git a/apps/web/app/api/revalidate/route.ts b/apps/web/app/api/revalidate/route.ts index 2f24caad8..d6afc0645 100644 --- a/apps/web/app/api/revalidate/route.ts +++ b/apps/web/app/api/revalidate/route.ts @@ -21,7 +21,7 @@ export async function POST(request: NextRequest) { }); } - const [video] = await db + const [video] = await db() .select() .from(videos) .where(eq(videos.id, videoId)); diff --git a/apps/web/app/api/screenshot/route.ts b/apps/web/app/api/screenshot/route.ts new file mode 100644 index 000000000..f2badabea --- /dev/null +++ b/apps/web/app/api/screenshot/route.ts @@ -0,0 +1,130 @@ +import { type NextRequest } from "next/server"; +import { db } from "@cap/database"; +import { s3Buckets, videos } from "@cap/database/schema"; +import { eq } from "drizzle-orm"; +import { + S3Client, + ListObjectsV2Command, + GetObjectCommand, +} from "@aws-sdk/client-s3"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { getHeaders } from "@/utils/helpers"; +import { createS3Client, getS3Bucket } from "@/utils/s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { S3_BUCKET_URL } from "@cap/utils"; +import { serverEnv } from "@cap/env"; + +export const revalidate = 0; + +export async function OPTIONS(request: NextRequest) { + const origin = request.headers.get("origin") as string; + + return new Response(null, { + status: 200, + headers: getHeaders(origin), + }); +} + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const userId = searchParams.get("userId") || ""; + const videoId = searchParams.get("screenshotId") || ""; + const origin = request.headers.get("origin") as string; + + if (!userId || !videoId) { + return new Response( + JSON.stringify({ + error: true, + message: "userId or videoId not supplied", + }), + { status: 401, headers: getHeaders(origin) } + ); + } + + const query = await db() + .select({ video: videos, bucket: s3Buckets }) + .from(videos) + .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) + .where(eq(videos.id, videoId)); + + if (query.length === 0) { + return new Response( + JSON.stringify({ error: true, message: "Video does not exist" }), + { status: 401, headers: getHeaders(origin) } + ); + } + + const result = query[0]; + if (!result?.video) { + return new Response( + JSON.stringify({ error: true, message: "Video not found" }), + { status: 401, headers: getHeaders(origin) } + ); + } + + const { video, bucket } = result; + + if (video.public === false) { + const user = await getCurrentUser(); + + if (!user || user.id !== video.ownerId) { + return new Response( + JSON.stringify({ error: true, message: "Video is not public" }), + { status: 401, headers: getHeaders(origin) } + ); + } + } + + const Bucket = await getS3Bucket(bucket); + const screenshotPrefix = `${userId}/${videoId}/`; + + try { + const [s3Client] = await createS3Client(bucket); + + const objectsCommand = new ListObjectsV2Command({ + Bucket, + Prefix: screenshotPrefix, + }); + + const objects = await s3Client.send(objectsCommand); + + const screenshot = objects.Contents?.find((object) => + object.Key?.endsWith(".png") + ); + + if (!screenshot) { + return new Response( + JSON.stringify({ error: true, message: "Screenshot not found" }), + { status: 404, headers: getHeaders(origin) } + ); + } + + let screenshotUrl: string; + + if (video.awsBucket !== serverEnv().CAP_AWS_BUCKET) { + screenshotUrl = await getSignedUrl( + s3Client, + new GetObjectCommand({ + Bucket, + Key: screenshot.Key, + }), + { expiresIn: 3600 } + ); + } else { + screenshotUrl = `${S3_BUCKET_URL}/${screenshot.Key}`; + } + + return new Response(JSON.stringify({ url: screenshotUrl }), { + status: 200, + headers: getHeaders(origin), + }); + } catch (error) { + return new Response( + JSON.stringify({ + error: error, + message: "Error generating screenshot URL", + }), + { status: 500, headers: getHeaders(origin) } + ); + } +} diff --git a/apps/web/app/api/selfhosted/migrations/route.ts b/apps/web/app/api/selfhosted/migrations/route.ts new file mode 100644 index 000000000..db28d63f0 --- /dev/null +++ b/apps/web/app/api/selfhosted/migrations/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { db } from "@cap/database"; +import { migrate } from "drizzle-orm/mysql2/migrator"; +import path from "path"; +import { serverEnv } from "@cap/env"; + +const migrations = { + run: false, +}; + +export async function POST() { + if (migrations.run) { + console.log(" ✅ DB migrations triggered but already run, skipping"); + return NextResponse.json({ + message: "✅ DB migrations already run, skipping", + }); + } + + const isDockerBuild = serverEnv().DOCKER_BUILD === "true"; + if (isDockerBuild) { + try { + console.log("🔍 DB migrations triggered"); + console.log("💿 Running DB migrations..."); + + await migrate(db() as any, { + migrationsFolder: path.join(process.cwd(), "/migrations"), + }); + migrations.run = true; + console.log("💿 Migrations run successfully!"); + return NextResponse.json({ + message: "✅ DB migrations run successfully!", + }); + } catch (error) { + console.error("🚨 MIGRATION_FAILED", { error }); + return NextResponse.json( + { + message: "🚨 DB migrations failed", + error: error instanceof Error ? error.message : String(error), + }, + { status: 500 } + ); + } + } + + migrations.run = true; + + return NextResponse.json({ + message: "DB migrations dont need to run in this environment", + }); +} diff --git a/apps/web/app/api/settings/billing/manage/route.ts b/apps/web/app/api/settings/billing/manage/route.ts index fdee85d6c..9c47b94c9 100644 --- a/apps/web/app/api/settings/billing/manage/route.ts +++ b/apps/web/app/api/settings/billing/manage/route.ts @@ -4,7 +4,7 @@ import { NextRequest, NextResponse } from "next/server"; import { eq } from "drizzle-orm"; import { db } from "@cap/database"; import { users } from "@cap/database/schema"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; export async function POST(request: NextRequest) { const user = await getCurrentUser(); @@ -17,14 +17,14 @@ export async function POST(request: NextRequest) { } if (!user.stripeCustomerId) { - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id, }, }); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id, @@ -34,9 +34,9 @@ export async function POST(request: NextRequest) { customerId = customer.id; } - const { url } = await stripe.billingPortal.sessions.create({ + const { url } = await stripe().billingPortal.sessions.create({ customer: customerId as string, - return_url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/dashboard/settings/workspace`, + return_url: `${serverEnv().WEB_URL}/dashboard/settings/workspace`, }); return NextResponse.json(url); } diff --git a/apps/web/app/api/settings/billing/quota/route.ts b/apps/web/app/api/settings/billing/quota/route.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/web/app/api/settings/billing/subscribe/route.ts b/apps/web/app/api/settings/billing/subscribe/route.ts index ef98c6faa..9f617aac5 100644 --- a/apps/web/app/api/settings/billing/subscribe/route.ts +++ b/apps/web/app/api/settings/billing/subscribe/route.ts @@ -4,7 +4,7 @@ import { NextRequest } from "next/server"; import { eq } from "drizzle-orm"; import { db } from "@cap/database"; import { users } from "@cap/database/schema"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; import posthog from "posthog-js"; export async function POST(request: NextRequest) { @@ -47,7 +47,7 @@ export async function POST(request: NextRequest) { if (!user.stripeCustomerId) { console.log("Creating new Stripe customer for user:", user.id); - const customer = await stripe.customers.create({ + const customer = await stripe().customers.create({ email: user.email, metadata: { userId: user.id, @@ -56,7 +56,7 @@ export async function POST(request: NextRequest) { console.log("Created Stripe customer:", customer.id); - await db + await db() .update(users) .set({ stripeCustomerId: customer.id, @@ -68,12 +68,12 @@ export async function POST(request: NextRequest) { } console.log("Creating checkout session for customer:", customerId); - const checkoutSession = await stripe.checkout.sessions.create({ + const checkoutSession = await stripe().checkout.sessions.create({ customer: customerId as string, line_items: [{ price: priceId, quantity: quantity }], mode: "subscription", - success_url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/dashboard/caps?upgrade=true`, - cancel_url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/pricing`, + success_url: `${serverEnv().WEB_URL}/dashboard/caps?upgrade=true`, + cancel_url: `${serverEnv().WEB_URL}/pricing`, allow_promotion_codes: true, }); diff --git a/apps/web/app/api/settings/billing/usage/route.ts b/apps/web/app/api/settings/billing/usage/route.ts index 2a0bb87e8..0452694e2 100644 --- a/apps/web/app/api/settings/billing/usage/route.ts +++ b/apps/web/app/api/settings/billing/usage/route.ts @@ -5,14 +5,16 @@ import { count, eq } from "drizzle-orm"; import { db } from "@cap/database"; import { videos } from "@cap/database/schema"; -export async function GET(request: NextRequest) { +export const dynamic = "force-dynamic"; + +export async function GET() { const user = await getCurrentUser(); if (!user) { return Response.json({ auth: false }, { status: 401 }); } - const numberOfVideos = await db + const numberOfVideos = await db() .select({ count: count() }) .from(videos) .where(eq(videos.ownerId, user.id)); diff --git a/apps/web/app/api/settings/onboarding/route.ts b/apps/web/app/api/settings/onboarding/route.ts index 434908db3..4e1e37453 100644 --- a/apps/web/app/api/settings/onboarding/route.ts +++ b/apps/web/app/api/settings/onboarding/route.ts @@ -15,7 +15,7 @@ export async function POST(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - await db + await db() .update(users) .set({ name: firstName, @@ -28,7 +28,7 @@ export async function POST(request: NextRequest) { fullName += ` ${lastName}`; } - const [space] = await db + const [space] = await db() .select() .from(spaces) .where(or(eq(spaces.ownerId, user.id), eq(spaceMembers.userId, user.id))) @@ -37,20 +37,22 @@ export async function POST(request: NextRequest) { if (!space) { const spaceId = nanoId(); - await db.insert(spaces).values({ - id: spaceId, - ownerId: user.id, - name: `${fullName}'s Space`, - }); + await db() + .insert(spaces) + .values({ + id: spaceId, + ownerId: user.id, + name: `${fullName}'s Space`, + }); - await db.insert(spaceMembers).values({ + await db().insert(spaceMembers).values({ id: nanoId(), userId: user.id, role: "owner", spaceId, }); - await db + await db() .update(users) .set({ activeSpaceId: spaceId }) .where(eq(users.id, user.id)); diff --git a/apps/web/app/api/settings/user/name/route.ts b/apps/web/app/api/settings/user/name/route.ts index 70de72548..3f2c51d5b 100644 --- a/apps/web/app/api/settings/user/name/route.ts +++ b/apps/web/app/api/settings/user/name/route.ts @@ -14,7 +14,7 @@ export async function POST(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - await db + await db() .update(users) .set({ name: firstName, diff --git a/apps/web/app/api/thumbnail/route.ts b/apps/web/app/api/thumbnail/route.ts index 4972769b2..6bc4af261 100644 --- a/apps/web/app/api/thumbnail/route.ts +++ b/apps/web/app/api/thumbnail/route.ts @@ -7,7 +7,7 @@ import { s3Buckets, videos } from "@cap/database/schema"; import { createS3Client, getS3Bucket } from "@/utils/s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { S3_BUCKET_URL } from "@cap/utils"; -import { clientEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; export const revalidate = 0; @@ -30,7 +30,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, @@ -65,10 +65,7 @@ export async function GET(request: NextRequest) { let thumbnailUrl: string; - if ( - !result.bucket || - video.awsBucket === clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET - ) { + if (!result.bucket || video.awsBucket === serverEnv().CAP_AWS_BUCKET) { thumbnailUrl = `${S3_BUCKET_URL}/${prefix}screenshot/screen-capture.jpg`; return new Response(JSON.stringify({ screen: thumbnailUrl }), { status: 200, @@ -77,7 +74,7 @@ export async function GET(request: NextRequest) { } const Bucket = await getS3Bucket(result.bucket); - const s3Client = await createS3Client(result.bucket); + const [s3Client] = await createS3Client(result.bucket); try { const listCommand = new ListObjectsV2Command({ diff --git a/apps/web/app/api/upload/multipart/[...route]/route.ts b/apps/web/app/api/upload/multipart/[...route]/route.ts index a968557db..719f48280 100644 --- a/apps/web/app/api/upload/multipart/[...route]/route.ts +++ b/apps/web/app/api/upload/multipart/[...route]/route.ts @@ -13,7 +13,7 @@ import { eq } from "drizzle-orm"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; import { handle } from "hono/vercel"; import { withAuth, corsMiddleware } from "@/app/api/utils"; @@ -265,7 +265,7 @@ app.post( const videoId = fileKey.split("/")[1]; if (videoId) { try { - await fetch(`${clientEnv.NEXT_PUBLIC_WEB_URL}/api/revalidate`, { + await fetch(`${serverEnv().WEB_URL}/api/revalidate`, { method: "POST", headers: { "Content-Type": "application/json", @@ -322,7 +322,7 @@ app.post( ); async function getUserBucketWithClient(userId: string) { - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, userId)); diff --git a/apps/web/app/api/upload/mux/create/route.ts b/apps/web/app/api/upload/mux/create/route.ts index ffa5289d3..191b2f2fc 100644 --- a/apps/web/app/api/upload/mux/create/route.ts +++ b/apps/web/app/api/upload/mux/create/route.ts @@ -8,10 +8,10 @@ import { CreateJobCommand, } from "@aws-sdk/client-mediaconvert"; import { createS3Client, getS3Bucket } from "@/utils/s3"; -import { serverEnv, clientEnv } from "@cap/env"; +import { serverEnv, buildEnv } from "@cap/env"; const allowedOrigins = [ - clientEnv.NEXT_PUBLIC_WEB_URL, + buildEnv.NEXT_PUBLIC_WEB_URL, "http://localhost:3001", "tauri://localhost", "http://tauri.localhost", @@ -66,7 +66,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets, @@ -148,7 +148,7 @@ export async function GET(request: NextRequest) { ); if (videoSegmentKeys.length > 149) { - await db + await db() .update(videos) .set({ skipProcessing: true }) .where(eq(videos.id, videoId)); @@ -174,17 +174,17 @@ export async function GET(request: NextRequest) { ); const mediaConvertClient = new MediaConvertClient({ - region: clientEnv.NEXT_PUBLIC_CAP_AWS_REGION || "", + region: serverEnv().CAP_AWS_REGION || "", credentials: { - accessKeyId: serverEnv.CAP_AWS_ACCESS_KEY || "", - secretAccessKey: serverEnv.CAP_AWS_SECRET_KEY || "", + accessKeyId: serverEnv().CAP_AWS_ACCESS_KEY || "", + secretAccessKey: serverEnv().CAP_AWS_SECRET_KEY || "", }, }); const outputKey = `${userId}/${videoId}/output/`; const createJobCommand = new CreateJobCommand({ - Role: serverEnv.CAP_AWS_MEDIACONVERT_ROLE_ARN || "", + Role: serverEnv().CAP_AWS_MEDIACONVERT_ROLE_ARN || "", Settings: { Inputs: videoSegmentKeys.map((videoSegmentKey, index) => { const audioSegmentKey = audioSegmentKeys[index]; @@ -267,7 +267,7 @@ export async function GET(request: NextRequest) { const createJobResponse = await mediaConvertClient.send(createJobCommand); const jobId = createJobResponse.Job?.Id; - await db.update(videos).set({ jobId }).where(eq(videos.id, videoId)); + await db().update(videos).set({ jobId }).where(eq(videos.id, videoId)); return new Response(JSON.stringify({ jobId: jobId }), { status: 200, diff --git a/apps/web/app/api/upload/mux/status/route.ts b/apps/web/app/api/upload/mux/status/route.ts index be1b30216..b0a89146f 100644 --- a/apps/web/app/api/upload/mux/status/route.ts +++ b/apps/web/app/api/upload/mux/status/route.ts @@ -6,10 +6,10 @@ import { MediaConvertClient, GetJobCommand, } from "@aws-sdk/client-mediaconvert"; -import { serverEnv, clientEnv } from "@cap/env"; +import { serverEnv, buildEnv } from "@cap/env"; const allowedOrigins = [ - clientEnv.NEXT_PUBLIC_WEB_URL, + buildEnv.NEXT_PUBLIC_WEB_URL, "http://localhost:3001", "tauri://localhost", "http://tauri.localhost", @@ -64,7 +64,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db.select().from(videos).where(eq(videos.id, videoId)); + const query = await db().select().from(videos).where(eq(videos.id, videoId)); if (query.length === 0) { return new Response( @@ -118,10 +118,10 @@ export async function GET(request: NextRequest) { } const mediaConvertClient = new MediaConvertClient({ - region: clientEnv.NEXT_PUBLIC_CAP_AWS_REGION || "", + region: serverEnv().CAP_AWS_REGION || "", credentials: { - accessKeyId: serverEnv.CAP_AWS_ACCESS_KEY || "", - secretAccessKey: serverEnv.CAP_AWS_SECRET_KEY || "", + accessKeyId: serverEnv().CAP_AWS_ACCESS_KEY || "", + secretAccessKey: serverEnv().CAP_AWS_SECRET_KEY || "", }, }); @@ -133,7 +133,7 @@ export async function GET(request: NextRequest) { const jobResponse = await mediaConvertClient.send(getJobCommand); const jobStatus = jobResponse.Job?.Status; - await db.update(videos).set({ jobStatus }).where(eq(videos.id, videoId)); + await db().update(videos).set({ jobStatus }).where(eq(videos.id, videoId)); return new Response(JSON.stringify({ jobStatus: jobStatus }), { status: 200, diff --git a/apps/web/app/api/upload/new/route.ts b/apps/web/app/api/upload/new/route.ts index 0d36064c7..f9889290b 100644 --- a/apps/web/app/api/upload/new/route.ts +++ b/apps/web/app/api/upload/new/route.ts @@ -1,7 +1,7 @@ import { type NextRequest } from "next/server"; import { getCurrentUser } from "@cap/database/auth/session"; import { uploadToS3 } from "@/utils/video/upload/helpers"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; export async function POST(request: NextRequest) { const user = await getCurrentUser(); @@ -14,8 +14,8 @@ export async function POST(request: NextRequest) { const videoCodec = formData.get("videoCodec"); const audioCodec = formData.get("audioCodec"); - const awsRegion = clientEnv.NEXT_PUBLIC_CAP_AWS_REGION; - const awsBucket = clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET; + const awsRegion = serverEnv().CAP_AWS_REGION; + const awsBucket = serverEnv().CAP_AWS_BUCKET; if (!user || !awsRegion || !awsBucket || !filename || !blobData) { console.error("Missing required data in /api/upload/new/route.ts"); diff --git a/apps/web/app/api/upload/signed/route.ts b/apps/web/app/api/upload/signed/route.ts index 3bbf512c8..0e20d5aa1 100644 --- a/apps/web/app/api/upload/signed/route.ts +++ b/apps/web/app/api/upload/signed/route.ts @@ -13,7 +13,7 @@ import { s3Buckets } from "@cap/database/schema"; import { eq } from "drizzle-orm"; import { cookies } from "next/headers"; import type { NextRequest } from "next/server"; -import { clientEnv, serverEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; export async function POST(request: NextRequest) { try { @@ -48,7 +48,7 @@ export async function POST(request: NextRequest) { } try { - const [bucket] = await db + const [bucket] = await db() .select() .from(s3Buckets) .where(eq(s3Buckets.ownerId, user.id)); @@ -65,17 +65,17 @@ export async function POST(request: NextRequest) { if ( !bucket || !s3Config || - bucket.bucketName !== clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET + bucket.bucketName !== serverEnv().CAP_AWS_BUCKET ) { - const distributionId = serverEnv.CAP_CLOUDFRONT_DISTRIBUTION_ID; + const distributionId = serverEnv().CAP_CLOUDFRONT_DISTRIBUTION_ID; if (distributionId) { console.log("Creating CloudFront invalidation for", fileKey); const cloudfront = new CloudFrontClient({ - region: clientEnv.NEXT_PUBLIC_CAP_AWS_REGION || "us-east-1", + region: serverEnv().CAP_AWS_REGION || "us-east-1", credentials: { - accessKeyId: serverEnv.CAP_AWS_ACCESS_KEY || "", - secretAccessKey: serverEnv.CAP_AWS_SECRET_KEY || "", + accessKeyId: serverEnv().CAP_AWS_ACCESS_KEY || "", + secretAccessKey: serverEnv().CAP_AWS_SECRET_KEY || "", }, }); @@ -146,13 +146,11 @@ export async function POST(request: NextRequest) { // When not using aws s3, we need to transform the url to the local endpoint if ( - clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET_URL && - !clientEnv.NEXT_PUBLIC_CAP_AWS_ENDPOINT?.endsWith( - "s3-accelerate.amazonaws.com" - ) + serverEnv().CAP_AWS_BUCKET_URL && + !serverEnv().CAP_AWS_ENDPOINT?.endsWith("s3-accelerate.amazonaws.com") ) { - const endpoint = clientEnv.NEXT_PUBLIC_CAP_AWS_ENDPOINT; - const bucket = clientEnv.NEXT_PUBLIC_CAP_AWS_BUCKET; + const endpoint = serverEnv().CAP_AWS_ENDPOINT; + const bucket = serverEnv().CAP_AWS_BUCKET; const newUrl = `${endpoint}/${bucket}/`; presignedPostData.url = newUrl; } @@ -163,7 +161,7 @@ export async function POST(request: NextRequest) { const videoId = fileKey.split("/")[1]; // Assuming fileKey format is userId/videoId/... if (videoId) { try { - await fetch(`${clientEnv.NEXT_PUBLIC_WEB_URL}/api/revalidate`, { + await fetch(`${serverEnv().WEB_URL}/api/revalidate`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/apps/web/app/api/utils.ts b/apps/web/app/api/utils.ts index dc62e11a6..81446247e 100644 --- a/apps/web/app/api/utils.ts +++ b/apps/web/app/api/utils.ts @@ -1,7 +1,7 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { cookies } from "next/headers"; import { createMiddleware } from "hono/factory"; -import { clientEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import { cors } from "hono/cors"; import { getServerSession, Session } from "next-auth"; import { authOptions } from "@cap/database/auth/auth-options"; @@ -20,7 +20,7 @@ async function getAuth(c: Context) { }); } - const session = await getServerSession(authOptions); + const session = await getServerSession(authOptions()); if (!session) return; const user = await getCurrentUser(session); if (!user) return; @@ -60,7 +60,7 @@ export const withAuth = createMiddleware<{ }); const allowedOrigins = [ - clientEnv.NEXT_PUBLIC_WEB_URL, + buildEnv.NEXT_PUBLIC_WEB_URL, "http://localhost:3001", "http://localhost:3000", "tauri://localhost", diff --git a/apps/web/app/api/video/comment/delete/route.ts b/apps/web/app/api/video/comment/delete/route.ts index 22ca9ca58..7b361146d 100644 --- a/apps/web/app/api/video/comment/delete/route.ts +++ b/apps/web/app/api/video/comment/delete/route.ts @@ -20,7 +20,7 @@ export async function DELETE(request: NextRequest) { try { // First, verify that the comment belongs to the user - const query = await db + const query = await db() .select() .from(comments) .where(and(eq(comments.id, commentId), eq(comments.authorId, user.id))); @@ -36,7 +36,7 @@ export async function DELETE(request: NextRequest) { } // Delete the comment and all its replies - await db + await db() .delete(comments) .where( or(eq(comments.id, commentId), eq(comments.parentCommentId, commentId)) diff --git a/apps/web/app/api/video/comment/route.ts b/apps/web/app/api/video/comment/route.ts index 3e5cc81ee..ab4268c46 100644 --- a/apps/web/app/api/video/comment/route.ts +++ b/apps/web/app/api/video/comment/route.ts @@ -5,7 +5,7 @@ import { comments } from "@cap/database/schema"; import { db } from "@cap/database"; import { rateLimitMiddleware } from "@/utils/helpers"; import { headers } from "next/headers"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; async function handlePost(request: NextRequest) { const user = await getCurrentUser(); @@ -44,7 +44,7 @@ async function handlePost(request: NextRequest) { updatedAt: new Date(), }; - await db.insert(comments).values(newComment); + await db().insert(comments).values(newComment); // Trigger email notification for new comment if (type === "text" && userId !== "anonymous") { @@ -52,7 +52,7 @@ async function handlePost(request: NextRequest) { // Don't await this to avoid blocking the response const absoluteUrl = new URL( "/api/email/new-comment", - clientEnv.NEXT_PUBLIC_WEB_URL + serverEnv().WEB_URL ).toString(); fetch(absoluteUrl, { method: "POST", diff --git a/apps/web/app/api/video/delete/route.ts b/apps/web/app/api/video/delete/route.ts index 7f82a8d12..49e84ee62 100644 --- a/apps/web/app/api/video/delete/route.ts +++ b/apps/web/app/api/video/delete/route.ts @@ -24,7 +24,7 @@ export async function DELETE(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - const query = await db + const query = await db() .select({ video: videos, bucket: s3Buckets }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) @@ -51,7 +51,7 @@ export async function DELETE(request: NextRequest) { ); } - await db + await db() .delete(videos) .where(and(eq(videos.id, videoId), eq(videos.ownerId, userId))); diff --git a/apps/web/app/api/video/domain-info/route.ts b/apps/web/app/api/video/domain-info/route.ts index 56c7c2147..c45f49a86 100644 --- a/apps/web/app/api/video/domain-info/route.ts +++ b/apps/web/app/api/video/domain-info/route.ts @@ -13,7 +13,7 @@ export async function GET(request: NextRequest) { try { // First, get the video to find the owner or shared space - const video = await db + const video = await db() .select({ id: videos.id, ownerId: videos.ownerId, @@ -32,7 +32,7 @@ export async function GET(request: NextRequest) { } // Check if the video is shared with a space - const sharedVideo = await db + const sharedVideo = await db() .select({ spaceId: sharedVideos.spaceId, }) @@ -47,7 +47,7 @@ export async function GET(request: NextRequest) { // If we have a space ID, get the space's custom domain if (spaceId) { - const space = await db + const space = await db() .select({ customDomain: spaces.customDomain, domainVerified: spaces.domainVerified, @@ -65,7 +65,7 @@ export async function GET(request: NextRequest) { } // If no shared space or no custom domain, check the owner's space - const ownerSpaces = await db + const ownerSpaces = await db() .select({ customDomain: spaces.customDomain, domainVerified: spaces.domainVerified, diff --git a/apps/web/app/api/video/metadata/route.ts b/apps/web/app/api/video/metadata/route.ts index 79e9eac7f..22c095469 100644 --- a/apps/web/app/api/video/metadata/route.ts +++ b/apps/web/app/api/video/metadata/route.ts @@ -15,7 +15,7 @@ export async function PUT(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - const query = await db.select().from(videos).where(eq(videos.id, videoId)); + const query = await db().select().from(videos).where(eq(videos.id, videoId)); if (query.length === 0) { return Response.json({ error: true }, { status: 401 }); @@ -30,7 +30,7 @@ export async function PUT(request: NextRequest) { return Response.json({ error: true }, { status: 401 }); } - await db + await db() .update(videos) .set({ metadata: metadata, diff --git a/apps/web/app/api/video/playlistUrl/route.ts b/apps/web/app/api/video/playlistUrl/route.ts index f6ce15aa6..0095a9ccb 100644 --- a/apps/web/app/api/video/playlistUrl/route.ts +++ b/apps/web/app/api/video/playlistUrl/route.ts @@ -5,7 +5,7 @@ import { eq } from "drizzle-orm"; import { getHeaders } from "@/utils/helpers"; import { CACHE_CONTROL_HEADERS } from "@/utils/helpers"; import { S3_BUCKET_URL } from "@cap/utils"; -import { clientEnv } from "@cap/env"; +import { serverEnv } from "@cap/env"; export const revalidate = 0; @@ -37,7 +37,7 @@ export async function GET(request: NextRequest) { ); } - const query = await db.select().from(videos).where(eq(videos.id, videoId)); + const query = await db().select().from(videos).where(eq(videos.id, videoId)); if (query.length === 0) { return new Response( @@ -76,8 +76,12 @@ export async function GET(request: NextRequest) { return new Response( JSON.stringify({ - playlistOne: `${clientEnv.NEXT_PUBLIC_WEB_URL}/api/playlist?userId=${video.ownerId}&videoId=${video.id}&videoType=video`, - playlistTwo: `${clientEnv.NEXT_PUBLIC_WEB_URL}/api/playlist?userId=${video.ownerId}&videoId=${video.id}&videoType=audio`, + playlistOne: `${serverEnv().WEB_URL}/api/playlist?userId=${ + video.ownerId + }&videoId=${video.id}&videoType=video`, + playlistTwo: `${serverEnv().WEB_URL}/api/playlist?userId=${ + video.ownerId + }&videoId=${video.id}&videoType=audio`, }), { status: 200, diff --git a/apps/web/app/api/video/transcribe/status/route.ts b/apps/web/app/api/video/transcribe/status/route.ts index 8cb2877a6..bdcb93c99 100644 --- a/apps/web/app/api/video/transcribe/status/route.ts +++ b/apps/web/app/api/video/transcribe/status/route.ts @@ -5,6 +5,8 @@ import { count, eq } from "drizzle-orm"; import { db } from "@cap/database"; import { videos } from "@cap/database/schema"; +export const dynamic = "force-dynamic"; + export async function GET(request: NextRequest) { const user = await getCurrentUser(); const url = new URL(request.url); @@ -21,7 +23,7 @@ export async function GET(request: NextRequest) { ); } - const video = await db.select().from(videos).where(eq(videos.id, videoId)); + const video = await db().select().from(videos).where(eq(videos.id, videoId)); if (video.length === 0 || !video[0]) { return Response.json( diff --git a/apps/web/app/api/waitlist/route.ts b/apps/web/app/api/waitlist/route.ts index 679b8ec15..164fa60ab 100644 --- a/apps/web/app/api/waitlist/route.ts +++ b/apps/web/app/api/waitlist/route.ts @@ -14,7 +14,7 @@ export async function POST(request: Request) { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${serverEnv.NEXT_LOOPS_KEY}`, + Authorization: `Bearer ${serverEnv().NEXT_LOOPS_KEY}`, }, body: JSON.stringify({ email, diff --git a/apps/web/app/api/webhooks/stripe/route.ts b/apps/web/app/api/webhooks/stripe/route.ts index cb998f08f..350b12ad3 100644 --- a/apps/web/app/api/webhooks/stripe/route.ts +++ b/apps/web/app/api/webhooks/stripe/route.ts @@ -4,7 +4,7 @@ import { users } from "@cap/database/schema"; import { eq } from "drizzle-orm"; import { NextResponse } from "next/server"; import Stripe from "stripe"; -import { serverEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import { PostHog } from "posthog-node"; const relevantEvents = new Set([ @@ -29,7 +29,7 @@ async function findUserWithRetry( // Try finding by userId first if available if (userId) { console.log(`Attempting to find user by ID: ${userId}`); - const userById = await db + const userById = await db() .select() .from(users) .where(eq(users.id, userId)) @@ -46,7 +46,7 @@ async function findUserWithRetry( // If not found by ID or no ID provided, try email if (email) { console.log(`Attempting to find user by email: ${email}`); - const userByEmail = await db + const userByEmail = await db() .select() .from(users) .where(eq(users.email, email)) @@ -89,7 +89,7 @@ export const POST = async (req: Request) => { console.log("Webhook received"); const buf = await req.text(); const sig = req.headers.get("Stripe-Signature") as string; - const webhookSecret = serverEnv.STRIPE_WEBHOOK_SECRET; + const webhookSecret = serverEnv().STRIPE_WEBHOOK_SECRET; let event: Stripe.Event; try { @@ -99,7 +99,7 @@ export const POST = async (req: Request) => { status: 400, }); } - event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); + event = stripe().webhooks.constructEvent(buf, sig, webhookSecret); console.log(`✅ Event received: ${event.type}`); } catch (err: any) { console.log(`❌ Error message: ${err.message}`); @@ -117,7 +117,7 @@ export const POST = async (req: Request) => { subscriptionId: session.subscription, }); - const customer = await stripe.customers.retrieve( + const customer = await stripe().customers.retrieve( session.customer as string ); console.log("Retrieved customer:", { @@ -162,7 +162,7 @@ export const POST = async (req: Request) => { name: dbUser.name, }); - const subscription = await stripe.subscriptions.retrieve( + const subscription = await stripe().subscriptions.retrieve( session.subscription as string ); console.log("Retrieved subscription:", { @@ -182,7 +182,7 @@ export const POST = async (req: Request) => { inviteQuota, }); - await db + await db() .update(users) .set({ stripeSubscriptionId: session.subscription as string, @@ -198,8 +198,8 @@ export const POST = async (req: Request) => { try { // Initialize server-side PostHog const serverPostHog = new PostHog( - process.env.NEXT_PUBLIC_POSTHOG_KEY || "", - { host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "" } + buildEnv.NEXT_PUBLIC_POSTHOG_KEY || "", + { host: buildEnv.NEXT_PUBLIC_POSTHOG_HOST || "" } ); // Track subscription completed event @@ -235,7 +235,7 @@ export const POST = async (req: Request) => { customerId: subscription.customer, }); - const customer = await stripe.customers.retrieve( + const customer = await stripe().customers.retrieve( subscription.customer as string ); console.log("Retrieved customer:", { @@ -281,7 +281,7 @@ export const POST = async (req: Request) => { }); // Get all active subscriptions for this customer - const subscriptions = await stripe.subscriptions.list({ + const subscriptions = await stripe().subscriptions.list({ customer: customer.id, status: "active", }); @@ -308,7 +308,7 @@ export const POST = async (req: Request) => { inviteQuota, }); - await db + await db() .update(users) .set({ stripeSubscriptionId: subscription.id, @@ -326,7 +326,7 @@ export const POST = async (req: Request) => { if (event.type === "customer.subscription.deleted") { const subscription = event.data.object as Stripe.Subscription; - const customer = await stripe.customers.retrieve( + const customer = await stripe().customers.retrieve( subscription.customer as string ); let foundUserId; @@ -336,7 +336,7 @@ export const POST = async (req: Request) => { if (!foundUserId) { console.log("No user found in metadata, checking customer email"); if ("email" in customer && customer.email) { - const userByEmail = await db + const userByEmail = await db() .select() .from(users) .where(eq(users.email, customer.email)) @@ -346,7 +346,7 @@ export const POST = async (req: Request) => { foundUserId = userByEmail[0].id; console.log(`User found by email: ${foundUserId}`); // Update customer metadata with userId - await stripe.customers.update(customer.id, { + await stripe().customers.update(customer.id, { metadata: { userId: foundUserId }, }); } else { @@ -363,7 +363,7 @@ export const POST = async (req: Request) => { } } - const userResult = await db + const userResult = await db() .select() .from(users) .where(eq(users.id, foundUserId)); @@ -373,7 +373,7 @@ export const POST = async (req: Request) => { return new Response("No user found", { status: 400 }); } - await db + await db() .update(users) .set({ stripeSubscriptionId: subscription.id, diff --git a/apps/web/app/blog/[slug]/page.tsx b/apps/web/app/blog/[slug]/page.tsx index 687442c6a..e4f16d765 100644 --- a/apps/web/app/blog/[slug]/page.tsx +++ b/apps/web/app/blog/[slug]/page.tsx @@ -1,7 +1,7 @@ import { ReadyToGetStarted } from "@/components/ReadyToGetStarted"; import { getBlogPosts } from "@/utils/blog"; import { calculateReadingTime } from "@/utils/readTime"; -import { clientEnv } from "@cap/env"; +import { buildEnv } from "@cap/env"; import { format, parseISO } from "date-fns"; import type { Metadata } from "next"; import { MDXRemote } from "next-mdx-remote/rsc"; @@ -34,7 +34,7 @@ export async function generateMetadata({ description: string; image: string; }; - let ogImage = `${clientEnv.NEXT_PUBLIC_WEB_URL}${image}`; + let ogImage = `${buildEnv.NEXT_PUBLIC_WEB_URL}${image}`; return { title, @@ -44,7 +44,7 @@ export async function generateMetadata({ description, type: "article", publishedTime, - url: `${clientEnv.NEXT_PUBLIC_WEB_URL}/blog/${post.slug}`, + url: `${buildEnv.NEXT_PUBLIC_WEB_URL}/blog/${post.slug}`, images: [ { url: ogImage, @@ -103,7 +103,10 @@ export default async function PostPage({ params }: PostProps) {
- +
diff --git a/apps/web/app/blog/_components/Share.tsx b/apps/web/app/blog/_components/Share.tsx index 44062f8c7..7ee20567e 100644 --- a/apps/web/app/blog/_components/Share.tsx +++ b/apps/web/app/blog/_components/Share.tsx @@ -1,8 +1,7 @@ "use client"; -import { clientEnv } from "@cap/env"; -import { useState } from "react"; import toast from "react-hot-toast"; +import { useState } from "react"; interface ShareProps { post: { @@ -11,10 +10,10 @@ interface ShareProps { title: string; }; }; + url: string; } -export function Share({ post }: ShareProps) { - const shareUrl = `${clientEnv.NEXT_PUBLIC_WEB_URL}/blog/${post.slug}`; +export function Share({ post, url }: ShareProps) { const [copied, setCopied] = useState(false); return ( @@ -25,7 +24,7 @@ export function Share({ post }: ShareProps) {