aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/routers/webhooks.ts
blob: ab2a69089ae935e195e715875c28d6f86d5da05a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import { z } from "zod";

import { webhooksTable } from "@karakeep/db/schema";
import {
  zNewWebhookSchema,
  zUpdateWebhookSchema,
  zWebhookSchema,
} from "@karakeep/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" });
      }
    }),
});