Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/actions/organization/delete-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 2 additions & 6 deletions apps/web/app/api/desktop/[...route]/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand Down
16 changes: 9 additions & 7 deletions apps/web/app/api/playlist/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
31 changes: 20 additions & 11 deletions apps/web/app/api/upload/[...route]/multipart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Comment thread
oscartbeaumont marked this conversation as resolved.
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) =>
Expand All @@ -74,6 +75,7 @@ app.post(
c.json({ error: "Error initiating multipart upload" }, 500),
);
}),
Effect.provideService(CurrentUser, user),
runPromise,
);
if (resp) return resp;
Expand Down Expand Up @@ -230,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",
Expand All @@ -247,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`);
Expand Down Expand Up @@ -467,5 +471,10 @@ app.post(
);
}),
);
}).pipe(provideOptionalAuth, runPromise),
}).pipe(
provideOptionalAuth,
Effect.provideService(CurrentUser, user),
runPromise,
);
},
);
2 changes: 1 addition & 1 deletion apps/web/app/embed/[videoId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/s/[videoId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions packages/web-backend/src/S3Buckets/S3BucketAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export const createS3BucketAccess = Effect.gen(function* () {
UploadId: uploadId,
PartNumber: partNumber,
}),
{ expiresIn: 3600 },
),
),
),
Expand Down
21 changes: 17 additions & 4 deletions packages/web-backend/src/Videos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,34 @@ export class Videos extends Effect.Service<Videos>()("Videos", {
const policy = yield* VideosPolicy;
const s3Buckets = yield* S3Buckets;

const getById = (id: Video.VideoId) =>
const getByIdForViewer = (id: Video.VideoId) =>
repo
.getById(id)
.pipe(
Policy.withPublicPolicy(policy.canView(id)),
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.
*/
// This is only for external use since it does an access check,
// internal use should prefer the repo directly
getById,
getByIdForViewer,

/*
* 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.
Expand All @@ -56,7 +69,7 @@ export class Videos extends Effect.Service<Videos>()("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,
Expand Down Expand Up @@ -199,7 +212,7 @@ export class Videos extends Effect.Service<Videos>()("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",
Expand Down