aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/routers/lists.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/trpc/routers/lists.ts')
-rw-r--r--packages/trpc/routers/lists.ts157
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();
+ }),
});