From cddaefd9420507318d71f56355ff5a6648dcd951 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 19 Jan 2025 13:55:40 +0000 Subject: feat: Change webhooks to be configurable by users --- packages/trpc/routers/webhooks.ts | 124 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 packages/trpc/routers/webhooks.ts (limited to 'packages/trpc/routers/webhooks.ts') 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" }); + } + }), +}); -- cgit v1.2.3-70-g09d2