diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-01-19 13:55:40 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-01-19 19:06:48 +0000 |
| commit | cddaefd9420507318d71f56355ff5a6648dcd951 (patch) | |
| tree | cf196ef12c36fdb0502b5ebf0f722ab32de8e2c0 /packages/trpc | |
| parent | 64f24acb9a1835ea7f0bec241c233c3e4a202d46 (diff) | |
| download | karakeep-cddaefd9420507318d71f56355ff5a6648dcd951.tar.zst | |
feat: Change webhooks to be configurable by users
Diffstat (limited to 'packages/trpc')
| -rw-r--r-- | packages/trpc/routers/_app.ts | 2 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.ts | 6 | ||||
| -rw-r--r-- | packages/trpc/routers/webhooks.ts | 124 |
3 files changed, 132 insertions, 0 deletions
diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts index 91030d8e..0d555a65 100644 --- a/packages/trpc/routers/_app.ts +++ b/packages/trpc/routers/_app.ts @@ -8,6 +8,7 @@ import { listsAppRouter } from "./lists"; import { promptsAppRouter } from "./prompts"; import { tagsAppRouter } from "./tags"; import { usersAppRouter } from "./users"; +import { webhooksAppRouter } from "./webhooks"; export const appRouter = router({ bookmarks: bookmarksAppRouter, @@ -19,6 +20,7 @@ export const appRouter = router({ admin: adminAppRouter, feeds: feedsAppRouter, highlights: highlightsAppRouter, + webhooks: webhooksAppRouter, }); // export type definition of API export type AppRouter = typeof appRouter; diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index 349ff688..4426ab14 100644 --- a/packages/trpc/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -48,6 +48,7 @@ import { OpenAIQueue, triggerSearchDeletion, triggerSearchReindex, + triggerWebhook, } from "@hoarder/shared/queues"; import { getSearchIdxClient } from "@hoarder/shared/search"; import { parseSearchQuery } from "@hoarder/shared/searchQueryParser"; @@ -442,6 +443,7 @@ export const bookmarksAppRouter = router({ } } await triggerSearchReindex(bookmark.id); + await triggerWebhook(bookmark.id, "created"); return bookmark; }), @@ -474,6 +476,7 @@ export const bookmarksAppRouter = router({ }); } await triggerSearchReindex(input.bookmarkId); + await triggerWebhook(input.bookmarkId, "edited"); return res[0]; }), @@ -500,6 +503,7 @@ export const bookmarksAppRouter = router({ }); } await triggerSearchReindex(input.bookmarkId); + await triggerWebhook(input.bookmarkId, "edited"); }), deleteBookmark: authedProcedure @@ -1012,6 +1016,7 @@ export const bookmarksAppRouter = router({ .onConflictDoNothing(); await triggerSearchReindex(input.bookmarkId); + await triggerWebhook(input.bookmarkId, "edited"); return { bookmarkId: input.bookmarkId, attached: allIds, @@ -1254,6 +1259,7 @@ Content: ${bookmark.content ?? ""} }) .where(eq(bookmarks.id, input.bookmarkId)); await triggerSearchReindex(input.bookmarkId); + await triggerWebhook(input.bookmarkId, "edited"); return { bookmarkId: input.bookmarkId, diff --git a/packages/trpc/routers/webhooks.ts b/packages/trpc/routers/webhooks.ts new file mode 100644 index 00000000..173e4f5a --- /dev/null +++ b/packages/trpc/routers/webhooks.ts @@ -0,0 +1,124 @@ +import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { webhooksTable } from "@hoarder/db/schema"; +import { + zNewWebhookSchema, + zUpdateWebhookSchema, + zWebhookSchema, +} from "@hoarder/shared/types/webhooks"; + +import { authedProcedure, Context, router } from "../index"; + +function adaptWebhook(webhook: typeof webhooksTable.$inferSelect) { + const { token, ...rest } = webhook; + return { + ...rest, + hasToken: token !== null, + }; +} + +export const ensureWebhookOwnership = experimental_trpcMiddleware<{ + ctx: Context; + input: { webhookId: string }; +}>().create(async (opts) => { + const webhook = await opts.ctx.db.query.webhooksTable.findFirst({ + where: eq(webhooksTable.id, opts.input.webhookId), + columns: { + userId: true, + }, + }); + if (!opts.ctx.user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "User is not authorized", + }); + } + if (!webhook) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Webhook not found", + }); + } + if (webhook.userId != opts.ctx.user.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "User is not allowed to access resource", + }); + } + + return opts.next(); +}); + +export const webhooksAppRouter = router({ + create: authedProcedure + .input(zNewWebhookSchema) + .output(zWebhookSchema) + .mutation(async ({ input, ctx }) => { + const [webhook] = await ctx.db + .insert(webhooksTable) + .values({ + url: input.url, + events: input.events, + token: input.token ?? null, + userId: ctx.user.id, + }) + .returning(); + + return adaptWebhook(webhook); + }), + update: authedProcedure + .input(zUpdateWebhookSchema) + .output(zWebhookSchema) + .use(ensureWebhookOwnership) + .mutation(async ({ input, ctx }) => { + const webhook = await ctx.db + .update(webhooksTable) + .set({ + url: input.url, + events: input.events, + token: input.token, + }) + .where( + and( + eq(webhooksTable.userId, ctx.user.id), + eq(webhooksTable.id, input.webhookId), + ), + ) + .returning(); + if (webhook.length == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + return adaptWebhook(webhook[0]); + }), + list: authedProcedure + .output(z.object({ webhooks: z.array(zWebhookSchema) })) + .query(async ({ ctx }) => { + const webhooks = await ctx.db.query.webhooksTable.findMany({ + where: eq(webhooksTable.userId, ctx.user.id), + }); + return { webhooks: webhooks.map(adaptWebhook) }; + }), + delete: authedProcedure + .input( + z.object({ + webhookId: z.string(), + }), + ) + .use(ensureWebhookOwnership) + .mutation(async ({ input, ctx }) => { + const res = await ctx.db + .delete(webhooksTable) + .where( + and( + eq(webhooksTable.userId, ctx.user.id), + eq(webhooksTable.id, input.webhookId), + ), + ); + if (res.changes == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + }), +}); |
