diff --git a/packages/convex/files.ts b/packages/convex/files.ts index 81b2022..29b7152 100644 --- a/packages/convex/files.ts +++ b/packages/convex/files.ts @@ -1,26 +1,16 @@ import { v } from "convex/values" import type { Id } from "./_generated/dataModel" -import { authenticatedMutation, authenticatedQuery } from "./functions" +import { authenticatedMutation, authenticatedQuery, authorizedGet } from "./functions" import * as Directories from "./model/directories" import * as Files from "./model/files" import type { FileSystemItem } from "./model/filesystem" export const generateUploadUrl = authenticatedMutation({ handler: async (ctx) => { - // ctx.user and ctx.identity are automatically available return await ctx.storage.generateUploadUrl() }, }) -export const generateFileUrl = authenticatedQuery({ - args: { - storageId: v.id("_storage"), - }, - handler: async (ctx, { storageId }) => { - return await ctx.storage.getUrl(storageId) - }, -}) - export const fetchFiles = authenticatedQuery({ args: { directoryId: v.optional(v.id("directories")), @@ -46,6 +36,10 @@ export const fetchDirectory = authenticatedQuery({ directoryId: v.id("directories"), }, handler: async (ctx, { directoryId }) => { + const directory = await authorizedGet(ctx, directoryId) + if (!directory) { + throw new Error("Directory not found") + } return await Directories.fetch(ctx, { directoryId }) }, }) @@ -56,6 +50,11 @@ export const createDirectory = authenticatedMutation({ directoryId: v.id("directories"), }, handler: async (ctx, { name, directoryId }): Promise> => { + const parentDirectory = await authorizedGet(ctx, directoryId) + if (!parentDirectory) { + throw new Error("Parent directory not found") + } + return await Directories.create(ctx, { name, parentId: directoryId, @@ -72,6 +71,11 @@ export const saveFile = authenticatedMutation({ mimeType: v.optional(v.string()), }, handler: async (ctx, { name, storageId, directoryId, size, mimeType }) => { + const directory = await authorizedGet(ctx, directoryId) + if (!directory) { + throw new Error("Directory not found") + } + const now = Date.now() await ctx.db.insert("files", { @@ -94,6 +98,11 @@ export const renameFile = authenticatedMutation({ newName: v.string(), }, handler: async (ctx, { directoryId, itemId, newName }) => { + const file = await authorizedGet(ctx, itemId) + if (!file) { + throw new Error("File not found") + } + await Files.renameFile(ctx, { directoryId, itemId, newName }) }, }) diff --git a/packages/convex/filesystem.ts b/packages/convex/filesystem.ts index 70e773e..d85f99a 100644 --- a/packages/convex/filesystem.ts +++ b/packages/convex/filesystem.ts @@ -1,5 +1,5 @@ import { v } from "convex/values" -import { authenticatedMutation, authenticatedQuery } from "./functions" +import { authenticatedMutation, authenticatedQuery, authorizedGet } from "./functions" import * as Directories from "./model/directories" import * as Err from "./model/error" import * as Files from "./model/files" @@ -22,10 +22,7 @@ export const moveItems = authenticatedMutation({ items: v.array(VFileSystemHandle), }, handler: async (ctx, { targetDirectory: targetDirectoryHandle, items }) => { - const targetDirectory = await Directories.fetchHandle( - ctx, - targetDirectoryHandle, - ) + const targetDirectory = await authorizedGet(ctx, targetDirectoryHandle.id) if (!targetDirectory) { throw Err.create( Err.Code.DirectoryNotFound, @@ -69,6 +66,16 @@ export const moveToTrash = authenticatedMutation({ handles: v.array(VFileSystemHandle), }, handler: async (ctx, { handles }) => { + for (const handle of handles) { + const item = await authorizedGet(ctx, handle.id) + if (!item) { + throw Err.create( + Err.Code.NotFound, + `Item ${handle.id} not found`, + ) + } + } + // biome-ignore lint/suspicious/useIterableCallbackReturn: switch statement is exhaustive const promises = handles.map((handle) => { switch (handle.kind) { @@ -142,3 +149,17 @@ export const restoreItems = authenticatedMutation({ return await FileSystem.restoreItems(ctx, { handles }) }, }) + +export const fetchFileUrl = authenticatedQuery({ + args: { + fileId: v.id("files"), + }, + handler: async (ctx, { fileId }) => { + const file = await authorizedGet(ctx, fileId) + if (!file) { + throw Err.create(Err.Code.NotFound, "File not found") + } + + return await FileSystem.fetchFileUrl(ctx, { fileId }) + }, +}) diff --git a/packages/convex/functions.ts b/packages/convex/functions.ts index 00cfcf3..148a2fc 100644 --- a/packages/convex/functions.ts +++ b/packages/convex/functions.ts @@ -1,9 +1,15 @@ -import type { UserIdentity } from "convex/server" +import type { + DocumentByName, + TableNamesInDataModel, + UserIdentity, +} from "convex/server" +import type { GenericId } from "convex/values" import { customCtx, customMutation, customQuery, } from "convex-helpers/server/customFunctions" +import type { DataModel } from "./_generated/dataModel" import type { MutationCtx, QueryCtx } from "./_generated/server" import { mutation, query } from "./_generated/server" import { type AuthUser, userIdentityOrThrow, userOrThrow } from "./model/user" @@ -43,3 +49,19 @@ export const authenticatedMutation = customMutation( return { user, identity } }), ) + +/** + * Gets a document by its id and checks if the user is authorized to access it + * + * @returns The document associated with the id or null if the document is not found. + */ +export async function authorizedGet>( + ctx: AuthenticatedQueryCtx | AuthenticatedMutationCtx, + id: GenericId, +): Promise | null> { + const item = await ctx.db.get(id) + if (item && item.userId !== ctx.user._id) { + return null + } + return item +} diff --git a/packages/convex/model/directories.ts b/packages/convex/model/directories.ts index dce8004..4ebf4c4 100644 --- a/packages/convex/model/directories.ts +++ b/packages/convex/model/directories.ts @@ -3,6 +3,7 @@ import type { AuthenticatedMutationCtx, AuthenticatedQueryCtx, } from "../functions" +import { authorizedGet } from "../functions" import * as Err from "./error" import { type DirectoryHandle, @@ -27,8 +28,8 @@ export async function fetchHandle( ctx: AuthenticatedQueryCtx, handle: DirectoryHandle, ): Promise> { - const directory = await ctx.db.get(handle.id) - if (!directory || directory.userId !== ctx.user._id) { + const directory = await authorizedGet(ctx, handle.id) + if (!directory) { throw Err.create( Err.Code.DirectoryNotFound, `Directory ${handle.id} not found`, @@ -41,7 +42,7 @@ export async function fetch( ctx: AuthenticatedQueryCtx, { directoryId }: { directoryId: Id<"directories"> }, ): Promise { - const directory = await ctx.db.get(directoryId) + const directory = await authorizedGet(ctx, directoryId) if (!directory) { throw Err.create( Err.Code.DirectoryNotFound, @@ -57,7 +58,7 @@ export async function fetch( ] let parentDirId = directory.parentId while (parentDirId) { - const parentDir = await ctx.db.get(parentDirId) + const parentDir = await authorizedGet(ctx, parentDirId) if (parentDir) { path.push({ handle: newDirectoryHandle(parentDir._id), @@ -65,7 +66,7 @@ export async function fetch( }) parentDirId = parentDir.parentId } else { - throw Err.create(Err.Code.Internal) + throw Err.create(Err.Code.DirectoryNotFound, "Parent directory not found") } } @@ -132,7 +133,7 @@ export async function create( ctx: AuthenticatedMutationCtx, { name, parentId }: { name: string; parentId: Id<"directories"> }, ): Promise> { - const parentDir = await ctx.db.get(parentId) + const parentDir = await authorizedGet(ctx, parentId) if (!parentDir) { throw Err.create( Err.Code.DirectoryNotFound, @@ -180,7 +181,7 @@ export async function move( ) { const conflictCheckResults = await Promise.allSettled( sourceDirectories.map((directory) => - ctx.db.get(directory.id).then((d) => { + authorizedGet(ctx, directory.id).then((d) => { if (!d) { throw Err.create( Err.Code.DirectoryNotFound, diff --git a/packages/convex/model/files.ts b/packages/convex/model/files.ts index e716cea..21c2c36 100644 --- a/packages/convex/model/files.ts +++ b/packages/convex/model/files.ts @@ -1,5 +1,5 @@ import type { Doc, Id } from "../_generated/dataModel" -import type { AuthenticatedMutationCtx } from "../functions" +import { type AuthenticatedMutationCtx, authorizedGet } from "../functions" import * as Err from "./error" import type { DirectoryHandle, FileHandle } from "./filesystem" @@ -48,7 +48,7 @@ export async function move( ) { const conflictCheckResults = await Promise.allSettled( items.map((fileHandle) => - ctx.db.get(fileHandle.id).then((f) => { + authorizedGet(ctx, fileHandle.id).then((f) => { if (!f) { throw Err.create( Err.Code.FileNotFound, diff --git a/packages/convex/model/filesystem.ts b/packages/convex/model/filesystem.ts index f4f33f9..08237bf 100644 --- a/packages/convex/model/filesystem.ts +++ b/packages/convex/model/filesystem.ts @@ -4,6 +4,7 @@ import type { AuthenticatedMutationCtx, AuthenticatedQueryCtx, } from "../functions" +import { authorizedGet } from "../functions" import * as Directories from "./directories" import * as Err from "./error" import * as Files from "./files" @@ -295,3 +296,20 @@ export async function emptyTrash( ], }) } + +export async function fetchFileUrl( + ctx: AuthenticatedQueryCtx, + { fileId }: { fileId: Id<"files"> }, +): Promise { + const file = await authorizedGet(ctx, fileId) + if (!file) { + throw Err.create(Err.Code.NotFound, "file not found") + } + + const url = await ctx.storage.getUrl(file.storageId) + if (!url) { + throw Err.create(Err.Code.NotFound, "file not found") + } + + return url +} diff --git a/packages/web/src/files/image-preview-dialog.tsx b/packages/web/src/files/image-preview-dialog.tsx index b85bc17..1311f59 100644 --- a/packages/web/src/files/image-preview-dialog.tsx +++ b/packages/web/src/files/image-preview-dialog.tsx @@ -41,8 +41,8 @@ export function ImagePreviewDialog({ file: Doc<"files"> onClose: () => void }) { - const fileUrl = useConvexQuery(api.files.generateFileUrl, { - storageId: file.storageId, + const fileUrl = useConvexQuery(api.filesystem.fetchFileUrl, { + fileId: file._id, }) const setZoomLevel = useSetAtom(zoomLevelAtom)