diff --git a/.github/workflows/docker-build-web.yml b/.github/workflows/docker-build-web.yml index 85dfa83197..af6a883dd2 100644 --- a/.github/workflows/docker-build-web.yml +++ b/.github/workflows/docker-build-web.yml @@ -38,7 +38,6 @@ jobs: echo "NEXT_PUBLIC_DOCKER_BUILD=true" >> .env echo "NEXT_PUBLIC_CAP_AWS_BUCKET=capso" >> .env echo "NEXT_PUBLIC_CAP_AWS_REGION=us-east-1" >> .env - cat .env - name: Login to GitHub Container Registry uses: docker/login-action@v3 diff --git a/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx index 1375f9dba1..ebf3fc1835 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx @@ -22,6 +22,7 @@ import { shareCap } from "@/actions/caps/share"; import { useDashboardContext } from "@/app/(org)/dashboard/Contexts"; import type { Spaces } from "@/app/(org)/dashboard/dashboard-data"; import { Tooltip } from "@/components/Tooltip"; +import { usePublicEnv } from "@/utils/public-env"; interface SharingDialogProps { isOpen: boolean; diff --git a/apps/web/app/api/notifications/preferences/route.ts b/apps/web/app/api/notifications/preferences/route.ts index 87c994c07d..83d6c72b72 100644 --- a/apps/web/app/api/notifications/preferences/route.ts +++ b/apps/web/app/api/notifications/preferences/route.ts @@ -14,6 +14,8 @@ const PreferencesSchema = z.object({ }), }); +export const dynamic = "force-dynamic"; + export async function GET() { const currentUser = await getCurrentUser(); if (!currentUser) { diff --git a/apps/web/app/api/notifications/route.ts b/apps/web/app/api/notifications/route.ts index 353f17038c..0ea8748bee 100644 --- a/apps/web/app/api/notifications/route.ts +++ b/apps/web/app/api/notifications/route.ts @@ -21,6 +21,8 @@ type NotificationsKeysWithReplies = | Exclude<`${NotificationsKeys}s`, "replys"> | "replies"; +export const dynamic = "force-dynamic"; + export async function GET() { const currentUser = await getCurrentUser(); if (!currentUser) { diff --git a/apps/web/app/api/screenshot/route.ts b/apps/web/app/api/screenshot/route.ts index 4fd0c5ff05..00e1d0bda7 100644 --- a/apps/web/app/api/screenshot/route.ts +++ b/apps/web/app/api/screenshot/route.ts @@ -1,8 +1,6 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { s3Buckets, videos } from "@cap/database/schema"; -import { serverEnv } from "@cap/env"; -import { S3_BUCKET_URL } from "@cap/utils"; import { eq } from "drizzle-orm"; import type { NextRequest } from "next/server"; import { getHeaders } from "@/utils/helpers"; @@ -90,11 +88,7 @@ export async function GET(request: NextRequest) { let screenshotUrl: string; - if (video.awsBucket !== serverEnv().CAP_AWS_BUCKET) { - screenshotUrl = await bucketProvider.getSignedObjectUrl(screenshot.Key!); - } else { - screenshotUrl = `${S3_BUCKET_URL}/${screenshot.Key}`; - } + screenshotUrl = await bucketProvider.getSignedObjectUrl(screenshot.Key!); return new Response(JSON.stringify({ url: screenshotUrl }), { status: 200, diff --git a/apps/web/app/api/thumbnail/route.ts b/apps/web/app/api/thumbnail/route.ts index 2e8da7b177..8f33bded18 100644 --- a/apps/web/app/api/thumbnail/route.ts +++ b/apps/web/app/api/thumbnail/route.ts @@ -1,7 +1,5 @@ import { db } from "@cap/database"; import { s3Buckets, videos } from "@cap/database/schema"; -import { serverEnv } from "@cap/env"; -import { S3_BUCKET_URL } from "@cap/utils"; import { eq } from "drizzle-orm"; import type { NextRequest } from "next/server"; import { getHeaders } from "@/utils/helpers"; @@ -58,19 +56,7 @@ export async function GET(request: NextRequest) { ); } - const { video } = result; const prefix = `${userId}/${videoId}/`; - - let thumbnailUrl: string; - - 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, - headers: getHeaders(origin), - }); - } - const bucketProvider = await createBucketProvider(result.bucket); try { @@ -96,7 +82,12 @@ export async function GET(request: NextRequest) { ); } - thumbnailUrl = await bucketProvider.getSignedObjectUrl(thumbnailKey); + const thumbnailUrl = await bucketProvider.getSignedObjectUrl(thumbnailKey); + + return new Response(JSON.stringify({ screen: thumbnailUrl }), { + status: 200, + headers: getHeaders(origin), + }); } catch (error) { return new Response( JSON.stringify({ @@ -110,9 +101,4 @@ export async function GET(request: NextRequest) { }, ); } - - return new Response(JSON.stringify({ screen: thumbnailUrl }), { - status: 200, - headers: getHeaders(origin), - }); } diff --git a/apps/web/app/api/video/playlistUrl/route.ts b/apps/web/app/api/video/playlistUrl/route.ts deleted file mode 100644 index 712de88fcf..0000000000 --- a/apps/web/app/api/video/playlistUrl/route.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { db } from "@cap/database"; -import { videos } from "@cap/database/schema"; -import { serverEnv } from "@cap/env"; -import { S3_BUCKET_URL } from "@cap/utils"; -import { eq } from "drizzle-orm"; -import type { NextRequest } from "next/server"; -import { CACHE_CONTROL_HEADERS, getHeaders } from "@/utils/helpers"; - -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("videoId") || ""; - 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().from(videos).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 video = query[0]; - if (!video) { - return new Response( - JSON.stringify({ error: true, message: "Video not found" }), - { - status: 404, - headers: getHeaders(origin), - }, - ); - } - - if (video.jobStatus === "COMPLETE") { - const playlistUrl = `${S3_BUCKET_URL}/${video.ownerId}/${video.id}/output/video_recording_000_output.m3u8`; - return new Response( - JSON.stringify({ playlistOne: playlistUrl, playlistTwo: null }), - { - status: 200, - headers: { - ...getHeaders(origin), - ...CACHE_CONTROL_HEADERS, - }, - }, - ); - } - - return new Response( - JSON.stringify({ - 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, - headers: { - ...getHeaders(origin), - ...CACHE_CONTROL_HEADERS, - }, - }, - ); -} diff --git a/apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx b/apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx index a94617ae88..cc97a6ea72 100644 --- a/apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx +++ b/apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx @@ -21,7 +21,6 @@ import { parseVTT, type TranscriptEntry, } from "@/app/s/[videoId]/_components/utils/transcript-utils"; -import { usePublicEnv } from "@/utils/public-env"; declare global { interface Window { @@ -147,8 +146,6 @@ export const EmbedVideo = forwardRef< } }, [chapters]); - const publicEnv = usePublicEnv(); - let videoSrc: string; let enableCrossOrigin = false; @@ -163,9 +160,9 @@ export const EmbedVideo = forwardRef< ) { videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=master`; } else if (data.source.type === "MediaConvert") { - videoSrc = `${publicEnv.s3BucketUrl}/${data.ownerId}/${data.id}/output/video_recording_000.m3u8`; + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; } else { - videoSrc = `${publicEnv.s3BucketUrl}/${data.ownerId}/${data.id}/combined-source/stream.m3u8`; + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; } useEffect(() => { diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index fbd9611b3a..d342d8fd81 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -12,7 +12,6 @@ import { useState, } from "react"; import { UpgradeModal } from "@/components/UpgradeModal"; -import { usePublicEnv } from "@/utils/public-env"; import { CapVideoPlayer } from "./CapVideoPlayer"; import { HLSVideoPlayer } from "./HLSVideoPlayer"; import { @@ -123,8 +122,6 @@ export const ShareVideo = forwardRef< } }, [chapters]); - const publicEnv = usePublicEnv(); - let videoSrc: string; let enableCrossOrigin = false; @@ -139,9 +136,9 @@ export const ShareVideo = forwardRef< ) { videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=master`; } else if (data.source.type === "MediaConvert") { - videoSrc = `${publicEnv.s3BucketUrl}/${data.ownerId}/${data.id}/output/video_recording_000.m3u8`; + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; } else { - videoSrc = `${publicEnv.s3BucketUrl}/${data.ownerId}/${data.id}/combined-source/stream.m3u8`; + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; } return ( diff --git a/apps/web/utils/s3.ts b/apps/web/utils/s3.ts index 9002809dac..32ada2562f 100644 --- a/apps/web/utils/s3.ts +++ b/apps/web/utils/s3.ts @@ -19,7 +19,6 @@ import { type ListObjectsV2Output, type ObjectIdentifier, PutObjectCommand, - PutObjectCommandInput, type PutObjectCommandOutput, type PutObjectRequest, S3Client, @@ -262,13 +261,14 @@ function createS3Provider( ), ); }, - headObject: (key) => - getClient(true).then((client) => - client.send(new HeadObjectCommand({ Bucket: bucket, Key: key })), - ), - putObject: (key, body, fields) => - getClient(true).then((client) => - client.send( + async headObject(key: string) { + return await getClient(true).then((c) => + c.send(new HeadObjectCommand({ Bucket: bucket, Key: key })), + ); + }, + async putObject(key: string, body, fields) { + return await getClient(true).then((c) => + c.send( new PutObjectCommand({ Bucket: bucket, Key: key, @@ -276,10 +276,11 @@ function createS3Provider( ContentType: fields?.contentType, }), ), - ), - copyObject: (source, key, args) => - getClient(true).then((client) => - client.send( + ); + }, + async copyObject(source: string, key: string, args) { + return await getClient(true).then((c) => + c.send( new CopyObjectCommand({ Bucket: bucket, CopySource: source, @@ -287,8 +288,9 @@ function createS3Provider( ...args, }), ), - ), - deleteObject: (key) => + ); + }, + deleteObject: (key: string) => getClient(true).then((client) => client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })), ), diff --git a/packages/database/auth/auth-options.tsx b/packages/database/auth/auth-options.tsx index 85e0961b2a..31f797e93c 100644 --- a/packages/database/auth/auth-options.tsx +++ b/packages/database/auth/auth-options.tsx @@ -14,6 +14,7 @@ import { dub } from "../dub"; import { sendEmail } from "../emails/config"; import { nanoId } from "../helpers"; import { organizationMembers, organizations, users } from "../schema"; +import { isEmailAllowedForSignup } from "./domain-utils"; import { DrizzleAdapter } from "./drizzle-adapter"; export const config = { @@ -189,6 +190,37 @@ export const authOptions = (): NextAuthOptions => { }, }, callbacks: { + async signIn({ user, email, credentials }) { + const allowedDomains = serverEnv().CAP_ALLOWED_SIGNUP_DOMAINS; + if (!allowedDomains) return true; + + // Get email from either user object (OAuth) or email parameter (email provider) + const userEmail = + user?.email || + (typeof email === "string" + ? email + : typeof credentials?.email === "string" + ? credentials.email + : null); + if (!userEmail || typeof userEmail !== "string") return true; + + const [existingUser] = await db() + .select() + .from(users) + .where(eq(users.email, userEmail)) + .limit(1); + + // Only apply domain restrictions for new users, existing ones can always sign in + if ( + !existingUser && + !isEmailAllowedForSignup(userEmail, allowedDomains) + ) { + console.warn(`Signup blocked for email domain: ${userEmail}`); + return false; + } + + return true; + }, async session({ token, session }) { if (!session.user) return session;