diff --git a/apps/web/actions/videos/new-comment.ts b/apps/web/actions/videos/new-comment.ts index 6b7814a466..8cb9b62fe0 100644 --- a/apps/web/actions/videos/new-comment.ts +++ b/apps/web/actions/videos/new-comment.ts @@ -3,11 +3,15 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoId } from "@cap/database/helpers"; -import { comments } from "@cap/database/schema"; +import { comments, videos } from "@cap/database/schema"; +import { provideOptionalAuth, VideosPolicy } from "@cap/web-backend"; import type { ImageUpload } from "@cap/web-domain"; -import { Comment, type Video } from "@cap/web-domain"; +import { Comment, Policy, type Video } from "@cap/web-domain"; +import { eq } from "drizzle-orm"; +import { Effect, Exit } from "effect"; import { revalidatePath } from "next/cache"; import { createNotification } from "@/lib/Notification"; +import * as EffectRuntime from "@/lib/server"; export async function newComment(data: { content: string; @@ -37,6 +41,33 @@ export async function newComment(data: { if (!content || !videoId) { throw new Error("Content and videoId are required"); } + + // Authentication alone isn't authorization: without this, any logged-in user could + // comment on someone else's private video by guessing its id. + // + // This also fetches the video row (rather than gating a no-op) because + // canView returns true for a nonexistent videoId by design, so a bogus id + // would otherwise reach the insert below. + const accessExit = await Effect.gen(function* () { + const videosPolicy = yield* VideosPolicy; + return yield* Effect.promise(() => + db() + .select({ id: videos.id }) + .from(videos) + .where(eq(videos.id, videoId)) + .limit(1), + ).pipe(Policy.withPublicPolicy(videosPolicy.canView(videoId))); + }).pipe(provideOptionalAuth, EffectRuntime.runPromiseExit); + + if (Exit.isFailure(accessExit)) { + console.error("Video access check failed:", accessExit); + throw new Error("Video not found"); + } + + if (accessExit.value.length === 0) { + throw new Error("Video not found"); + } + const id = Comment.CommentId.make(nanoId()); const newComment = {