diff options
Diffstat (limited to 'packages/trpc/routers/lists.ts')
| -rw-r--r-- | packages/trpc/routers/lists.ts | 157 |
1 files changed, 139 insertions, 18 deletions
diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts index 7118c608..c9a19f30 100644 --- a/packages/trpc/routers/lists.ts +++ b/packages/trpc/routers/lists.ts @@ -9,14 +9,15 @@ import { } from "@karakeep/shared/types/lists"; import type { AuthedContext } from "../index"; -import { authedProcedure, router } from "../index"; +import { authedProcedure, createRateLimitMiddleware, router } from "../index"; import { List } from "../models/lists"; import { ensureBookmarkOwnership } from "./bookmarks"; -export const ensureListOwnership = experimental_trpcMiddleware<{ +export const ensureListAtLeastViewer = experimental_trpcMiddleware<{ ctx: AuthedContext; input: { listId: string }; }>().create(async (opts) => { + // This would throw if the user can't view the list const list = await List.fromId(opts.ctx, opts.input.listId); return opts.next({ ctx: { @@ -26,20 +27,41 @@ export const ensureListOwnership = experimental_trpcMiddleware<{ }); }); +export const ensureListAtLeastEditor = experimental_trpcMiddleware<{ + ctx: AuthedContext & { list: List }; + input: { listId: string }; +}>().create(async (opts) => { + opts.ctx.list.ensureCanEdit(); + return opts.next({ + ctx: opts.ctx, + }); +}); + +export const ensureListAtLeastOwner = experimental_trpcMiddleware<{ + ctx: AuthedContext & { list: List }; + input: { listId: string }; +}>().create(async (opts) => { + opts.ctx.list.ensureCanManage(); + return opts.next({ + ctx: opts.ctx, + }); +}); + export const listsAppRouter = router({ create: authedProcedure .input(zNewBookmarkListSchema) .output(zBookmarkListSchema) .mutation(async ({ input, ctx }) => { - return await List.create(ctx, input).then((l) => l.list); + return await List.create(ctx, input).then((l) => l.asZBookmarkList()); }), edit: authedProcedure .input(zEditBookmarkListSchemaWithValidation) .output(zBookmarkListSchema) - .use(ensureListOwnership) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) .mutation(async ({ input, ctx }) => { await ctx.list.update(input); - return ctx.list.list; + return ctx.list.asZBookmarkList(); }), merge: authedProcedure .input(zMergeListSchema) @@ -48,6 +70,8 @@ export const listsAppRouter = router({ List.fromId(ctx, input.sourceId), List.fromId(ctx, input.targetId), ]); + sourceList.ensureCanManage(); + targetList.ensureCanManage(); return await sourceList.mergeInto( targetList, input.deleteSourceAfterMerge, @@ -60,7 +84,8 @@ export const listsAppRouter = router({ deleteChildren: z.boolean().optional().default(false), }), ) - .use(ensureListOwnership) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) .mutation(async ({ ctx, input }) => { if (input.deleteChildren) { const children = await ctx.list.getChildren(); @@ -75,7 +100,8 @@ export const listsAppRouter = router({ bookmarkId: z.string(), }), ) - .use(ensureListOwnership) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastEditor) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { await ctx.list.addBookmark(input.bookmarkId); @@ -87,8 +113,8 @@ export const listsAppRouter = router({ bookmarkId: z.string(), }), ) - .use(ensureListOwnership) - .use(ensureBookmarkOwnership) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastEditor) .mutation(async ({ input, ctx }) => { await ctx.list.removeBookmark(input.bookmarkId); }), @@ -99,9 +125,9 @@ export const listsAppRouter = router({ }), ) .output(zBookmarkListSchema) - .use(ensureListOwnership) - .query(({ ctx }) => { - return ctx.list.list; + .use(ensureListAtLeastViewer) + .query(async ({ ctx }) => { + return ctx.list.asZBookmarkList(); }), list: authedProcedure .output( @@ -111,7 +137,7 @@ export const listsAppRouter = router({ ) .query(async ({ ctx }) => { const results = await List.getAll(ctx); - return { lists: results.map((l) => l.list) }; + return { lists: results.map((l) => l.asZBookmarkList()) }; }), getListsOfBookmark: authedProcedure .input(z.object({ bookmarkId: z.string() })) @@ -123,7 +149,7 @@ export const listsAppRouter = router({ .use(ensureBookmarkOwnership) .query(async ({ input, ctx }) => { const lists = await List.forBookmark(ctx, input.bookmarkId); - return { lists: lists.map((l) => l.list) }; + return { lists: lists.map((l) => l.asZBookmarkList()) }; }), stats: authedProcedure .output( @@ -134,7 +160,7 @@ export const listsAppRouter = router({ .query(async ({ ctx }) => { const lists = await List.getAll(ctx); const sizes = await Promise.all(lists.map((l) => l.getSize())); - return { stats: new Map(lists.map((l, i) => [l.list.id, sizes[i]])) }; + return { stats: new Map(lists.map((l, i) => [l.id, sizes[i]])) }; }), // Rss endpoints @@ -149,7 +175,8 @@ export const listsAppRouter = router({ token: z.string(), }), ) - .use(ensureListOwnership) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) .mutation(async ({ ctx }) => { const token = await ctx.list.regenRssToken(); return { token: token! }; @@ -160,7 +187,8 @@ export const listsAppRouter = router({ listId: z.string(), }), ) - .use(ensureListOwnership) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) .mutation(async ({ ctx }) => { await ctx.list.clearRssToken(); }), @@ -175,8 +203,101 @@ export const listsAppRouter = router({ token: z.string().nullable(), }), ) - .use(ensureListOwnership) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) .query(async ({ ctx }) => { return { token: await ctx.list.getRssToken() }; }), + + // Collaboration endpoints + addCollaborator: authedProcedure + .input( + z.object({ + listId: z.string(), + email: z.string().email(), + role: z.enum(["viewer", "editor"]), + }), + ) + .use( + createRateLimitMiddleware({ + name: "lists.addCollaborator", + windowMs: 15 * 60 * 1000, + maxRequests: 20, + }), + ) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) + .mutation(async ({ input, ctx }) => { + await ctx.list.addCollaboratorByEmail(input.email, input.role); + }), + removeCollaborator: authedProcedure + .input( + z.object({ + listId: z.string(), + userId: z.string(), + }), + ) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) + .mutation(async ({ input, ctx }) => { + await ctx.list.removeCollaborator(input.userId); + }), + updateCollaboratorRole: authedProcedure + .input( + z.object({ + listId: z.string(), + userId: z.string(), + role: z.enum(["viewer", "editor"]), + }), + ) + .use(ensureListAtLeastViewer) + .use(ensureListAtLeastOwner) + .mutation(async ({ input, ctx }) => { + await ctx.list.updateCollaboratorRole(input.userId, input.role); + }), + getCollaborators: authedProcedure + .input( + z.object({ + listId: z.string(), + }), + ) + .output( + z.object({ + collaborators: z.array( + z.object({ + id: z.string(), + userId: z.string(), + role: z.enum(["viewer", "editor"]), + addedAt: z.date(), + user: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + }), + }), + ), + owner: z + .object({ + id: z.string(), + name: z.string(), + email: z.string(), + }) + .nullable(), + }), + ) + .use(ensureListAtLeastViewer) + .query(async ({ ctx }) => { + return await ctx.list.getCollaborators(); + }), + + leaveList: authedProcedure + .input( + z.object({ + listId: z.string(), + }), + ) + .use(ensureListAtLeastViewer) + .mutation(async ({ ctx }) => { + await ctx.list.leaveList(); + }), }); |
