From 49b37541795d8f511f06534fa91bbb6235519810 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 01:09:02 +0800 Subject: [PATCH 01/11] fix deleting no parts --- apps/web/app/api/desktop/[...route]/video.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/web/app/api/desktop/[...route]/video.ts b/apps/web/app/api/desktop/[...route]/video.ts index dbb6bc7259..dd3e4c406d 100644 --- a/apps/web/app/api/desktop/[...route]/video.ts +++ b/apps/web/app/api/desktop/[...route]/video.ts @@ -20,11 +20,7 @@ import { Effect, Option } from "effect"; import { Hono } from "hono"; import { z } from "zod"; import { runPromise } from "@/lib/server"; -import { - isAtLeastSemver, - isFromDesktopSemver, - UPLOAD_PROGRESS_VERSION, -} from "@/utils/desktop"; +import { isFromDesktopSemver, UPLOAD_PROGRESS_VERSION } from "@/utils/desktop"; import { stringOrNumberOptional } from "@/utils/zod"; import { withAuth } from "../../utils"; @@ -295,7 +291,7 @@ app.delete( prefix: `${user.id}/${videoId}/`, }); - if (listedObjects.Contents?.length) + if (listedObjects.Contents && listedObjects.Contents.length > 1) yield* bucket.deleteObjects( listedObjects.Contents.map((content: any) => ({ Key: content.Key, From c1c1aa06b71320943a4b06e43c8805dcfcb83328 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 01:11:40 +0800 Subject: [PATCH 02/11] fix --- apps/web/actions/organization/delete-space.ts | 2 +- packages/web-backend/src/Videos/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/actions/organization/delete-space.ts b/apps/web/actions/organization/delete-space.ts index 406474c888..17b22e3cc2 100644 --- a/apps/web/actions/organization/delete-space.ts +++ b/apps/web/actions/organization/delete-space.ts @@ -77,7 +77,7 @@ export async function deleteSpace( prefix: `organizations/${user.activeOrganizationId}/spaces/${spaceId}/`, }); - if (listedObjects.Contents?.length) { + if (listedObjects.Contents && listedObjects.Contents.length > 1) { yield* bucket.deleteObjects( listedObjects.Contents.map((content) => ({ Key: content.Key, diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index 2dc85ac80d..0f2a0bed33 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -56,7 +56,7 @@ export class Videos extends Effect.Service()("Videos", { const listedObjects = yield* bucket.listObjects({ prefix }); - if (listedObjects.Contents?.length) { + if (listedObjects.Contents && listedObjects.Contents.length > 1) { yield* bucket.deleteObjects( listedObjects.Contents.map((content) => ({ Key: content.Key, From 435ba5afe38916dc1355fea434e18906f9201703 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 01:28:33 +0800 Subject: [PATCH 03/11] maybe fix multipart initiate --- apps/web/app/api/upload/[...route]/multipart.ts | 7 ++++--- packages/web-backend/src/Videos/index.ts | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index eb946b70a1..3de47455a4 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -47,14 +47,15 @@ app.post( }); const videoIdFromFileKey = fileKey.split("/")[1]; - const videoId = "videoId" in body ? body.videoId : videoIdFromFileKey; - if (!videoId) throw new Error("Video ID is required"); + const videoIdRaw = "videoId" in body ? body.videoId : videoIdFromFileKey; + if (!videoIdRaw) throw new Error("Video ID is required"); + const videoId = Video.VideoId.make(videoIdRaw); const resp = await Effect.gen(function* () { const videos = yield* Videos; const db = yield* Database; - const video = yield* videos.getById(Video.VideoId.make(videoId)); + const video = yield* videos.getByIdForOwner(videoId); if (Option.isNone(video)) return yield* new Video.NotFoundError(); yield* db.use((db) => diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index 0f2a0bed33..a8d813175e 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -24,6 +24,14 @@ export class Videos extends Effect.Service()("Videos", { Effect.withSpan("Videos.getById"), ); + const getByIdForOwner = (id: Video.VideoId) => + repo + .getById(id) + .pipe( + Policy.withPublicPolicy(policy.isOwner(id)), + Effect.withSpan("Videos.getByIdForOwner"), + ); + return { /* * Get a video by ID. Will fail if the user does not have access. @@ -32,6 +40,8 @@ export class Videos extends Effect.Service()("Videos", { // internal use should prefer the repo directly getById, + getByIdForOwner, + /* * Delete a video. Will fail if the user does not have access. */ From abbd8cb11f3574b315332d23d226d6cd92ef0c95 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 01:37:30 +0800 Subject: [PATCH 04/11] fix auth check for multipart initiate --- apps/web/app/api/upload/[...route]/multipart.ts | 3 ++- packages/web-backend/src/Videos/index.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index 3de47455a4..5f1027e9a9 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -12,7 +12,7 @@ import { S3Buckets, Videos, } from "@cap/web-backend"; -import { Video } from "@cap/web-domain"; +import { CurrentUser, Video } from "@cap/web-domain"; import { zValidator } from "@hono/zod-validator"; import { and, eq } from "drizzle-orm"; import { Effect, Option, Schedule } from "effect"; @@ -75,6 +75,7 @@ app.post( c.json({ error: "Error initiating multipart upload" }, 500), ); }), + Effect.provideService(CurrentUser, user), runPromise, ); if (resp) return resp; diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index a8d813175e..1e5d7f80d0 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -16,7 +16,7 @@ export class Videos extends Effect.Service()("Videos", { const policy = yield* VideosPolicy; const s3Buckets = yield* S3Buckets; - const getById = (id: Video.VideoId) => + const getByIdForViewer = (id: Video.VideoId) => repo .getById(id) .pipe( @@ -38,8 +38,11 @@ export class Videos extends Effect.Service()("Videos", { */ // This is only for external use since it does an access check, // internal use should prefer the repo directly - getById, + getById: getByIdForViewer, + /* + * Get a video by ID. Will fail if the user isn't the owner. + */ getByIdForOwner, /* @@ -209,7 +212,7 @@ export class Videos extends Effect.Service()("Videos", { getAnalytics: Effect.fn("Videos.getAnalytics")(function* ( videoId: Video.VideoId, ) { - const [video] = yield* getById(videoId).pipe( + const [video] = yield* getByIdForViewer(videoId).pipe( Effect.flatten, Effect.catchTag( "NoSuchElementException", From e96f38eb3efd5223d96a24876deb0cfc53d224f4 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 01:54:15 +0800 Subject: [PATCH 05/11] fix auth policy --- apps/web/app/api/playlist/route.ts | 16 +++++++------- .../app/api/upload/[...route]/multipart.ts | 21 ++++++++++++------- apps/web/app/embed/[videoId]/page.tsx | 2 +- apps/web/app/s/[videoId]/page.tsx | 2 +- packages/web-backend/src/Videos/index.ts | 2 +- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index fcd3e5a8d3..e59d4f0f0a 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -47,13 +47,15 @@ const ApiLive = HttpApiBuilder.api(Api).pipe( return handlers.handle("getVideoSrc", ({ urlParams }) => Effect.gen(function* () { - const [video] = yield* videos.getById(urlParams.videoId).pipe( - Effect.flatten, - Effect.catchTag( - "NoSuchElementException", - () => new HttpApiError.NotFound(), - ), - ); + const [video] = yield* videos + .getByIdForViewer(urlParams.videoId) + .pipe( + Effect.flatten, + Effect.catchTag( + "NoSuchElementException", + () => new HttpApiError.NotFound(), + ), + ); return yield* getPlaylistResponse(video, urlParams); }).pipe( diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index 5f1027e9a9..67d1b6d5cc 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -232,14 +232,14 @@ app.post( ]), ), ), - (c) => - Effect.gen(function* () { + (c) => { + const { uploadId, parts, ...body } = c.req.valid("json"); + const user = c.get("user"); + + return Effect.gen(function* () { const videos = yield* Videos; const db = yield* Database; - const { uploadId, parts, ...body } = c.req.valid("json"); - const user = c.get("user"); - const fileKey = parseVideoIdOrFileKey(user.id, { ...body, subpath: "result.mp4", @@ -249,7 +249,9 @@ app.post( const videoId = "videoId" in body ? body.videoId : videoIdFromFileKey; if (!videoId) throw new Error("Video ID is required"); - const maybeVideo = yield* videos.getById(Video.VideoId.make(videoId)); + const maybeVideo = yield* videos.getByIdForOwner( + Video.VideoId.make(videoId), + ); if (Option.isNone(maybeVideo)) { c.status(404); return c.text(`Video '${encodeURIComponent(videoId)}' not found`); @@ -469,5 +471,10 @@ app.post( ); }), ); - }).pipe(provideOptionalAuth, runPromise), + }).pipe( + provideOptionalAuth, + Effect.provideService(CurrentUser, user), + runPromise, + ); + }, ); diff --git a/apps/web/app/embed/[videoId]/page.tsx b/apps/web/app/embed/[videoId]/page.tsx index c5cc03f100..9244f8c2d0 100644 --- a/apps/web/app/embed/[videoId]/page.tsx +++ b/apps/web/app/embed/[videoId]/page.tsx @@ -29,7 +29,7 @@ export async function generateMetadata( const params = await props.params; const videoId = params.videoId as Video.VideoId; - return Effect.flatMap(Videos, (v) => v.getById(videoId)).pipe( + return Effect.flatMap(Videos, (v) => v.getByIdForViewer(videoId)).pipe( Effect.map( Option.match({ onNone: () => notFound(), diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index 81c089826b..8e2eae1089 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -134,7 +134,7 @@ export async function generateMetadata( referrer.includes(domain), ); - return Effect.flatMap(Videos, (v) => v.getById(videoId)).pipe( + return Effect.flatMap(Videos, (v) => v.getByIdForViewer(videoId)).pipe( Effect.map( Option.match({ onNone: () => notFound(), diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index 1e5d7f80d0..01388755f7 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -38,7 +38,7 @@ export class Videos extends Effect.Service()("Videos", { */ // This is only for external use since it does an access check, // internal use should prefer the repo directly - getById: getByIdForViewer, + getByIdForViewer, /* * Get a video by ID. Will fail if the user isn't the owner. From 336060f2534f91c0fc95be16828d63a76fc59070 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 02:35:05 +0800 Subject: [PATCH 06/11] bump URL expiry --- packages/web-backend/src/S3Buckets/S3BucketAccess.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web-backend/src/S3Buckets/S3BucketAccess.ts b/packages/web-backend/src/S3Buckets/S3BucketAccess.ts index 0e382d5064..0a745d933d 100644 --- a/packages/web-backend/src/S3Buckets/S3BucketAccess.ts +++ b/packages/web-backend/src/S3Buckets/S3BucketAccess.ts @@ -256,6 +256,7 @@ export const createS3BucketAccess = Effect.gen(function* () { UploadId: uploadId, PartNumber: partNumber, }), + { expiresIn: 3600 }, ), ), ), From aeb0f84741b4b4b4187d1731824c7ebbb041140f Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 02:43:11 +0800 Subject: [PATCH 07/11] fixes --- apps/web/actions/organization/delete-space.ts | 2 +- apps/web/app/api/desktop/[...route]/video.ts | 2 +- apps/web/app/api/playlist/route.ts | 2 +- apps/web/app/api/upload/[...route]/multipart.ts | 2 +- apps/web/app/embed/[videoId]/page.tsx | 2 +- apps/web/app/s/[videoId]/page.tsx | 2 +- packages/web-backend/src/S3Buckets/S3BucketAccess.ts | 2 +- packages/web-backend/src/Videos/index.ts | 8 ++++---- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/web/actions/organization/delete-space.ts b/apps/web/actions/organization/delete-space.ts index 17b22e3cc2..4d2c5ef3be 100644 --- a/apps/web/actions/organization/delete-space.ts +++ b/apps/web/actions/organization/delete-space.ts @@ -77,7 +77,7 @@ export async function deleteSpace( prefix: `organizations/${user.activeOrganizationId}/spaces/${spaceId}/`, }); - if (listedObjects.Contents && listedObjects.Contents.length > 1) { + if (listedObjects.Contents) { yield* bucket.deleteObjects( listedObjects.Contents.map((content) => ({ Key: content.Key, diff --git a/apps/web/app/api/desktop/[...route]/video.ts b/apps/web/app/api/desktop/[...route]/video.ts index dd3e4c406d..ba0f540169 100644 --- a/apps/web/app/api/desktop/[...route]/video.ts +++ b/apps/web/app/api/desktop/[...route]/video.ts @@ -291,7 +291,7 @@ app.delete( prefix: `${user.id}/${videoId}/`, }); - if (listedObjects.Contents && listedObjects.Contents.length > 1) + if (listedObjects.Contents) yield* bucket.deleteObjects( listedObjects.Contents.map((content: any) => ({ Key: content.Key, diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index e59d4f0f0a..7bf28deb68 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -48,7 +48,7 @@ const ApiLive = HttpApiBuilder.api(Api).pipe( return handlers.handle("getVideoSrc", ({ urlParams }) => Effect.gen(function* () { const [video] = yield* videos - .getByIdForViewer(urlParams.videoId) + .getByIdForViewing(urlParams.videoId) .pipe( Effect.flatten, Effect.catchTag( diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index 67d1b6d5cc..8360ed32e9 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -2,7 +2,7 @@ import { CloudFrontClient, CreateInvalidationCommand, } from "@aws-sdk/client-cloudfront"; -import { db, updateIfDefined } from "@cap/database"; +import { updateIfDefined } from "@cap/database"; import * as Db from "@cap/database/schema"; import { serverEnv } from "@cap/env"; import { diff --git a/apps/web/app/embed/[videoId]/page.tsx b/apps/web/app/embed/[videoId]/page.tsx index 9244f8c2d0..a48cdbfe07 100644 --- a/apps/web/app/embed/[videoId]/page.tsx +++ b/apps/web/app/embed/[videoId]/page.tsx @@ -29,7 +29,7 @@ export async function generateMetadata( const params = await props.params; const videoId = params.videoId as Video.VideoId; - return Effect.flatMap(Videos, (v) => v.getByIdForViewer(videoId)).pipe( + return Effect.flatMap(Videos, (v) => v.getByIdForViewing(videoId)).pipe( Effect.map( Option.match({ onNone: () => notFound(), diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index 8e2eae1089..b34286685e 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -134,7 +134,7 @@ export async function generateMetadata( referrer.includes(domain), ); - return Effect.flatMap(Videos, (v) => v.getByIdForViewer(videoId)).pipe( + return Effect.flatMap(Videos, (v) => v.getByIdForViewing(videoId)).pipe( Effect.map( Option.match({ onNone: () => notFound(), diff --git a/packages/web-backend/src/S3Buckets/S3BucketAccess.ts b/packages/web-backend/src/S3Buckets/S3BucketAccess.ts index 0a745d933d..2c7cd698db 100644 --- a/packages/web-backend/src/S3Buckets/S3BucketAccess.ts +++ b/packages/web-backend/src/S3Buckets/S3BucketAccess.ts @@ -181,7 +181,7 @@ export const createS3BucketAccess = Effect.gen(function* () { ), ), ), - ), + ).pipe(Effect.when(() => objects.length > 0)), getPresignedPutUrl: ( key: string, args?: Omit, diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index 01388755f7..3e9f8f3b05 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -16,7 +16,7 @@ export class Videos extends Effect.Service()("Videos", { const policy = yield* VideosPolicy; const s3Buckets = yield* S3Buckets; - const getByIdForViewer = (id: Video.VideoId) => + const getByIdForViewing = (id: Video.VideoId) => repo .getById(id) .pipe( @@ -38,7 +38,7 @@ export class Videos extends Effect.Service()("Videos", { */ // This is only for external use since it does an access check, // internal use should prefer the repo directly - getByIdForViewer, + getByIdForViewing, /* * Get a video by ID. Will fail if the user isn't the owner. @@ -69,7 +69,7 @@ export class Videos extends Effect.Service()("Videos", { const listedObjects = yield* bucket.listObjects({ prefix }); - if (listedObjects.Contents && listedObjects.Contents.length > 1) { + if (listedObjects.Contents) { yield* bucket.deleteObjects( listedObjects.Contents.map((content) => ({ Key: content.Key, @@ -212,7 +212,7 @@ export class Videos extends Effect.Service()("Videos", { getAnalytics: Effect.fn("Videos.getAnalytics")(function* ( videoId: Video.VideoId, ) { - const [video] = yield* getByIdForViewer(videoId).pipe( + const [video] = yield* getByIdForViewing(videoId).pipe( Effect.flatten, Effect.catchTag( "NoSuchElementException", From 587c7c573d66e52d5998f4b3435fdd01d750e9f7 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 02:56:50 +0800 Subject: [PATCH 08/11] fixes --- .../app/api/upload/[...route]/multipart.ts | 20 ++++++++++++------- apps/web/lib/server.ts | 5 +++-- packages/web-backend/src/index.ts | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index 8360ed32e9..54f92d7ea9 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -5,12 +5,14 @@ import { import { updateIfDefined } from "@cap/database"; import * as Db from "@cap/database/schema"; import { serverEnv } from "@cap/env"; +import { HttpApiError } from "@effect/platform"; import { AwsCredentials, Database, provideOptionalAuth, S3Buckets, Videos, + VideosRepo, } from "@cap/web-backend"; import { CurrentUser, Video } from "@cap/web-domain"; import { zValidator } from "@hono/zod-validator"; @@ -52,11 +54,14 @@ app.post( const videoId = Video.VideoId.make(videoIdRaw); const resp = await Effect.gen(function* () { - const videos = yield* Videos; + const user = yield* CurrentUser; + const repo = yield* VideosRepo; const db = yield* Database; - const video = yield* videos.getByIdForOwner(videoId); + const video = yield* repo.getById(videoId); if (Option.isNone(video)) return yield* new Video.NotFoundError(); + if (video.value[0].ownerId !== user.id) + return yield* new HttpApiError.Unauthorized(); yield* db.use((db) => db @@ -71,6 +76,11 @@ app.post( if (e._tag === "VideoNotFoundError") return Effect.succeed(c.text("Video not found", 404)); + if (e._tag === "Unauthorized") + return Effect.succeed( + c.text("User not authenticated", 401), + ); + return Effect.succeed( c.json({ error: "Error initiating multipart upload" }, 500), ); @@ -471,10 +481,6 @@ app.post( ); }), ); - }).pipe( - provideOptionalAuth, - Effect.provideService(CurrentUser, user), - runPromise, - ); + }).pipe(Effect.provideService(CurrentUser, user), runPromise); }, ); diff --git a/apps/web/lib/server.ts b/apps/web/lib/server.ts index c263de730f..a5cc1c887c 100644 --- a/apps/web/lib/server.ts +++ b/apps/web/lib/server.ts @@ -14,6 +14,7 @@ import { Videos, VideosPolicy, Workflows, + VideosRepo, } from "@cap/web-backend"; import { type HttpAuthMiddleware, Video } from "@cap/web-domain"; import { @@ -21,11 +22,10 @@ import { Headers, type HttpApi, HttpApiBuilder, - HttpApiClient, HttpMiddleware, HttpServer, } from "@effect/platform"; -import { RpcClient, RpcMessage, RpcMiddleware } from "@effect/rpc"; +import { RpcClient, RpcMiddleware } from "@effect/rpc"; import { Cause, Config, @@ -100,6 +100,7 @@ export const Dependencies = Layer.mergeAll( S3Buckets.Default, Videos.Default, VideosPolicy.Default, + VideosRepo.Default, Folders.Default, SpacesPolicy.Default, OrganisationsPolicy.Default, diff --git a/packages/web-backend/src/index.ts b/packages/web-backend/src/index.ts index 0898b86376..2a5e1f0037 100644 --- a/packages/web-backend/src/index.ts +++ b/packages/web-backend/src/index.ts @@ -11,4 +11,5 @@ export { Spaces } from "./Spaces/index.ts"; export { SpacesPolicy } from "./Spaces/SpacesPolicy.ts"; export { Videos } from "./Videos/index.ts"; export { VideosPolicy } from "./Videos/VideosPolicy.ts"; +export { VideosRepo } from "./Videos/VideosRepo.ts"; export * as Workflows from "./Workflows.ts"; From a0ba0ec305e24f429d27e675142ee5d00a085af7 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 03:07:54 +0800 Subject: [PATCH 09/11] fixes --- .../web/app/api/upload/[...route]/multipart.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index 54f92d7ea9..af66535d4e 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -5,7 +5,6 @@ import { import { updateIfDefined } from "@cap/database"; import * as Db from "@cap/database/schema"; import { serverEnv } from "@cap/env"; -import { HttpApiError } from "@effect/platform"; import { AwsCredentials, Database, @@ -13,8 +12,9 @@ import { S3Buckets, Videos, VideosRepo, + VideosPolicy, } from "@cap/web-backend"; -import { CurrentUser, Video } from "@cap/web-domain"; +import { CurrentUser, Policy, Video } from "@cap/web-domain"; import { zValidator } from "@hono/zod-validator"; import { and, eq } from "drizzle-orm"; import { Effect, Option, Schedule } from "effect"; @@ -54,14 +54,14 @@ app.post( const videoId = Video.VideoId.make(videoIdRaw); const resp = await Effect.gen(function* () { - const user = yield* CurrentUser; const repo = yield* VideosRepo; + const policy = yield* VideosPolicy; const db = yield* Database; - const video = yield* repo.getById(videoId); + const video = yield* repo + .getById(videoId) + .pipe(Policy.withPolicy(policy.isOwner(videoId))); if (Option.isNone(video)) return yield* new Video.NotFoundError(); - if (video.value[0].ownerId !== user.id) - return yield* new HttpApiError.Unauthorized(); yield* db.use((db) => db @@ -76,10 +76,8 @@ app.post( if (e._tag === "VideoNotFoundError") return Effect.succeed(c.text("Video not found", 404)); - if (e._tag === "Unauthorized") - return Effect.succeed( - c.text("User not authenticated", 401), - ); + // if (e._tag === "DatabaseError") + // return Effect.succeed(c.text("Video not found", 404)); return Effect.succeed( c.json({ error: "Error initiating multipart upload" }, 500), From 0baadd1158520de520145875de3295ad7a7220ac Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 03:17:53 +0800 Subject: [PATCH 10/11] fixes --- .../app/api/upload/[...route]/multipart.ts | 19 +++++++++---------- packages/web-backend/src/Videos/index.ts | 13 ------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index af66535d4e..29c0f56bf4 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -50,7 +50,7 @@ app.post( const videoIdFromFileKey = fileKey.split("/")[1]; const videoIdRaw = "videoId" in body ? body.videoId : videoIdFromFileKey; - if (!videoIdRaw) throw new Error("Video ID is required"); + if (!videoIdRaw) return c.text("Video id not found", 400); const videoId = Video.VideoId.make(videoIdRaw); const resp = await Effect.gen(function* () { @@ -76,9 +76,6 @@ app.post( if (e._tag === "VideoNotFoundError") return Effect.succeed(c.text("Video not found", 404)); - // if (e._tag === "DatabaseError") - // return Effect.succeed(c.text("Video not found", 404)); - return Effect.succeed( c.json({ error: "Error initiating multipart upload" }, 500), ); @@ -245,7 +242,8 @@ app.post( const user = c.get("user"); return Effect.gen(function* () { - const videos = yield* Videos; + const repo = yield* VideosRepo; + const policy = yield* VideosPolicy; const db = yield* Database; const fileKey = parseVideoIdOrFileKey(user.id, { @@ -254,12 +252,13 @@ app.post( }); const videoIdFromFileKey = fileKey.split("/")[1]; - const videoId = "videoId" in body ? body.videoId : videoIdFromFileKey; - if (!videoId) throw new Error("Video ID is required"); + const videoIdRaw = "videoId" in body ? body.videoId : videoIdFromFileKey; + if (!videoIdRaw) return c.text("Video id not found", 400); + const videoId = Video.VideoId.make(videoIdRaw); - const maybeVideo = yield* videos.getByIdForOwner( - Video.VideoId.make(videoId), - ); + const maybeVideo = yield* repo + .getById(videoId) + .pipe(Policy.withPolicy(policy.isOwner(videoId))); if (Option.isNone(maybeVideo)) { c.status(404); return c.text(`Video '${encodeURIComponent(videoId)}' not found`); diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index 3e9f8f3b05..b85a464e7e 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -24,14 +24,6 @@ export class Videos extends Effect.Service()("Videos", { Effect.withSpan("Videos.getById"), ); - const getByIdForOwner = (id: Video.VideoId) => - repo - .getById(id) - .pipe( - Policy.withPublicPolicy(policy.isOwner(id)), - Effect.withSpan("Videos.getByIdForOwner"), - ); - return { /* * Get a video by ID. Will fail if the user does not have access. @@ -40,11 +32,6 @@ export class Videos extends Effect.Service()("Videos", { // internal use should prefer the repo directly getByIdForViewing, - /* - * Get a video by ID. Will fail if the user isn't the owner. - */ - getByIdForOwner, - /* * Delete a video. Will fail if the user does not have access. */ From 6d2fc33e43e0e7f4bcfd50032a6af4608baa388f Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 22 Oct 2025 03:20:42 +0800 Subject: [PATCH 11/11] format --- apps/web/app/api/upload/[...route]/multipart.ts | 2 +- apps/web/lib/server.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/api/upload/[...route]/multipart.ts b/apps/web/app/api/upload/[...route]/multipart.ts index 29c0f56bf4..e3d39487e3 100644 --- a/apps/web/app/api/upload/[...route]/multipart.ts +++ b/apps/web/app/api/upload/[...route]/multipart.ts @@ -11,8 +11,8 @@ import { provideOptionalAuth, S3Buckets, Videos, - VideosRepo, VideosPolicy, + VideosRepo, } from "@cap/web-backend"; import { CurrentUser, Policy, Video } from "@cap/web-domain"; import { zValidator } from "@hono/zod-validator"; diff --git a/apps/web/lib/server.ts b/apps/web/lib/server.ts index a5cc1c887c..58c85cf4ef 100644 --- a/apps/web/lib/server.ts +++ b/apps/web/lib/server.ts @@ -13,8 +13,8 @@ import { SpacesPolicy, Videos, VideosPolicy, - Workflows, VideosRepo, + Workflows, } from "@cap/web-backend"; import { type HttpAuthMiddleware, Video } from "@cap/web-domain"; import {