aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/routers
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-01-19 13:55:40 +0000
committerMohamed Bassem <me@mbassem.com>2025-01-19 19:06:48 +0000
commitcddaefd9420507318d71f56355ff5a6648dcd951 (patch)
treecf196ef12c36fdb0502b5ebf0f722ab32de8e2c0 /packages/trpc/routers
parent64f24acb9a1835ea7f0bec241c233c3e4a202d46 (diff)
downloadkarakeep-cddaefd9420507318d71f56355ff5a6648dcd951.tar.zst
feat: Change webhooks to be configurable by users
Diffstat (limited to 'packages/trpc/routers')
-rw-r--r--packages/trpc/routers/_app.ts2
-rw-r--r--packages/trpc/routers/bookmarks.ts6
-rw-r--r--packages/trpc/routers/webhooks.ts124
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" });
+ }
+ }),
+});