* Add cardPublicId to card webhook payloads * fix: include card publicId in webhook payloads Add data.card.publicId to webhook payloads while keeping data.card.id for compatibility, and update card/webhook call sites and tests so webhook consumers can reliably use public IDs.
361 lines
9.2 KiB
TypeScript
361 lines
9.2 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod";
|
|
|
|
import * as webhookRepo from "@kan/db/repository/webhook.repo";
|
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
|
import { webhookEvents } from "@kan/db/schema";
|
|
|
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
|
import { assertPermission } from "../utils/permissions";
|
|
import {
|
|
webhookUrlSchema,
|
|
sendWebhookToUrl,
|
|
createCardWebhookPayload,
|
|
} from "../utils/webhook";
|
|
|
|
const webhookEventSchema = z.enum(webhookEvents);
|
|
|
|
export const webhookRouter = createTRPCRouter({
|
|
list: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Get all webhooks for a workspace",
|
|
method: "GET",
|
|
path: "/workspaces/{workspacePublicId}/webhooks",
|
|
description: "Retrieves all webhooks configured for a workspace",
|
|
tags: ["Webhooks"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
|
.output(
|
|
z.array(
|
|
z.object({
|
|
publicId: z.string(),
|
|
name: z.string(),
|
|
url: z.string(),
|
|
events: z.array(webhookEventSchema),
|
|
active: z.boolean(),
|
|
createdAt: z.date(),
|
|
updatedAt: z.date().nullable(),
|
|
}),
|
|
),
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: "User not authenticated",
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
ctx.db,
|
|
input.workspacePublicId,
|
|
);
|
|
|
|
if (!workspace)
|
|
throw new TRPCError({
|
|
message: "Workspace not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertPermission(ctx.db, userId, workspace.id, "workspace:manage");
|
|
|
|
return webhookRepo.getAllByWorkspaceId(ctx.db, workspace.id);
|
|
}),
|
|
|
|
create: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Create a webhook",
|
|
method: "POST",
|
|
path: "/workspaces/{workspacePublicId}/webhooks",
|
|
description: "Creates a new webhook for a workspace",
|
|
tags: ["Webhooks"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
workspacePublicId: z.string().min(12),
|
|
name: z.string().min(1).max(255),
|
|
url: webhookUrlSchema,
|
|
secret: z.string().max(512).optional(),
|
|
events: z.array(webhookEventSchema).min(1),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
publicId: z.string(),
|
|
name: z.string(),
|
|
url: z.string(),
|
|
events: z.array(webhookEventSchema),
|
|
active: z.boolean(),
|
|
createdAt: z.date(),
|
|
}),
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: "User not authenticated",
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
ctx.db,
|
|
input.workspacePublicId,
|
|
);
|
|
|
|
if (!workspace)
|
|
throw new TRPCError({
|
|
message: "Workspace not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertPermission(ctx.db, userId, workspace.id, "workspace:manage");
|
|
|
|
const result = await webhookRepo.create(ctx.db, {
|
|
workspaceId: workspace.id,
|
|
name: input.name,
|
|
url: input.url,
|
|
secret: input.secret,
|
|
events: input.events,
|
|
createdBy: userId,
|
|
});
|
|
|
|
if (!result)
|
|
throw new TRPCError({
|
|
message: "Unable to create webhook",
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
});
|
|
|
|
return result;
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Update a webhook",
|
|
method: "PUT",
|
|
path: "/workspaces/{workspacePublicId}/webhooks/{webhookPublicId}",
|
|
description: "Updates a webhook by its public ID",
|
|
tags: ["Webhooks"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
workspacePublicId: z.string().min(12),
|
|
webhookPublicId: z.string().min(12),
|
|
name: z.string().min(1).max(255).optional(),
|
|
url: webhookUrlSchema.optional(),
|
|
secret: z.string().max(512).optional(),
|
|
events: z.array(webhookEventSchema).min(1).optional(),
|
|
active: z.boolean().optional(),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
publicId: z.string(),
|
|
name: z.string(),
|
|
url: z.string(),
|
|
events: z.array(webhookEventSchema),
|
|
active: z.boolean(),
|
|
createdAt: z.date(),
|
|
updatedAt: z.date().nullable(),
|
|
}),
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: "User not authenticated",
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
ctx.db,
|
|
input.workspacePublicId,
|
|
);
|
|
|
|
if (!workspace)
|
|
throw new TRPCError({
|
|
message: "Workspace not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertPermission(ctx.db, userId, workspace.id, "workspace:manage");
|
|
|
|
const webhook = await webhookRepo.getByPublicId(
|
|
ctx.db,
|
|
input.webhookPublicId,
|
|
);
|
|
|
|
if (!webhook || webhook.workspaceId !== workspace.id)
|
|
throw new TRPCError({
|
|
message: "Webhook not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
const result = await webhookRepo.update(ctx.db, input.webhookPublicId, {
|
|
name: input.name,
|
|
url: input.url,
|
|
secret: input.secret,
|
|
events: input.events,
|
|
active: input.active,
|
|
});
|
|
|
|
if (!result)
|
|
throw new TRPCError({
|
|
message: "Unable to update webhook",
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
});
|
|
|
|
return result;
|
|
}),
|
|
|
|
delete: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Delete a webhook",
|
|
method: "DELETE",
|
|
path: "/workspaces/{workspacePublicId}/webhooks/{webhookPublicId}",
|
|
description: "Deletes a webhook by its public ID",
|
|
tags: ["Webhooks"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
workspacePublicId: z.string().min(12),
|
|
webhookPublicId: z.string().min(12),
|
|
}),
|
|
)
|
|
.output(z.object({ success: z.boolean() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: "User not authenticated",
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
ctx.db,
|
|
input.workspacePublicId,
|
|
);
|
|
|
|
if (!workspace)
|
|
throw new TRPCError({
|
|
message: "Workspace not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertPermission(ctx.db, userId, workspace.id, "workspace:manage");
|
|
|
|
const webhook = await webhookRepo.getByPublicId(
|
|
ctx.db,
|
|
input.webhookPublicId,
|
|
);
|
|
|
|
if (!webhook || webhook.workspaceId !== workspace.id)
|
|
throw new TRPCError({
|
|
message: "Webhook not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await webhookRepo.hardDelete(ctx.db, input.webhookPublicId);
|
|
|
|
return { success: true };
|
|
}),
|
|
|
|
test: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Test a webhook",
|
|
method: "POST",
|
|
path: "/workspaces/{workspacePublicId}/webhooks/{webhookPublicId}/test",
|
|
description: "Sends a test payload to a webhook",
|
|
tags: ["Webhooks"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
workspacePublicId: z.string().min(12),
|
|
webhookPublicId: z.string().min(12),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
success: z.boolean(),
|
|
statusCode: z.number().optional(),
|
|
error: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: "User not authenticated",
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
ctx.db,
|
|
input.workspacePublicId,
|
|
);
|
|
|
|
if (!workspace)
|
|
throw new TRPCError({
|
|
message: "Workspace not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertPermission(ctx.db, userId, workspace.id, "workspace:manage");
|
|
|
|
const webhook = await webhookRepo.getByPublicId(
|
|
ctx.db,
|
|
input.webhookPublicId,
|
|
);
|
|
|
|
if (!webhook || webhook.workspaceId !== workspace.id)
|
|
throw new TRPCError({
|
|
message: "Webhook not found",
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
const testPayload = createCardWebhookPayload("card.created", {
|
|
id: "test-card-id",
|
|
publicId: "test-card-public-id",
|
|
title: "Test Card",
|
|
description: "This is a test webhook payload",
|
|
dueDate: null,
|
|
listId: "test-list-id",
|
|
}, {
|
|
boardId: "test-board-id",
|
|
boardName: "Test Board",
|
|
listName: "Test List",
|
|
user: {
|
|
id: userId,
|
|
name: ctx.user?.name ?? "Test User",
|
|
},
|
|
});
|
|
|
|
const result = await sendWebhookToUrl(
|
|
webhook.url,
|
|
webhook.secret ?? undefined,
|
|
testPayload,
|
|
);
|
|
|
|
return result;
|
|
}),
|
|
});
|