feat: Allow http webhook urls when self-hosting (#12499)

This commit is contained in:
Tom Moor
2026-05-27 22:52:15 -04:00
committed by GitHub
parent 76a3ba4e83
commit 82743b1c0a
4 changed files with 56 additions and 3 deletions
@@ -13,6 +13,7 @@ import Input from "~/components/Input";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useMobile from "~/hooks/useMobile";
import isCloudHosted from "~/utils/isCloudHosted";
import Flex from "@shared/components/Flex";
const WEBHOOK_EVENTS = {
@@ -153,6 +154,9 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
});
const events = watch("events");
const url = watch("url");
const showInsecureUrlWarning =
!isCloudHosted && typeof url === "string" && url.startsWith("http://");
const selectedGroups = filter(events, (e) => !e.includes("."));
const isAllEventSelected = includes(events, "*");
const filteredEvents = filter(events, (e) => {
@@ -224,9 +228,16 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
<Input
required
flex
pattern="https://.*"
pattern={isCloudHosted ? "https://.*" : "https?://.*"}
placeholder="https://…"
label={t("URL")}
error={
showInsecureUrlWarning
? t(
"Webhook delivery over http is insecure, use https if possible"
)
: undefined
}
{...register("url", { required: true })}
/>
<Input
+9 -2
View File
@@ -1,7 +1,14 @@
import { z } from "zod";
import env from "@server/env";
import { WebhookSubscription } from "@server/models";
import { BaseSchema } from "@server/routes/api/schema";
const webhookUrl = z
.url()
.refine((val) => !env.isCloudHosted || val.startsWith("https://"), {
error: "Webhook url must use https",
});
export const WebhookSubscriptionsListSchema = BaseSchema.extend({
body: z.object({
/** Webhook subscriptions sorting direction */
@@ -33,7 +40,7 @@ export type WebhookSubscriptionsListReq = z.infer<
export const WebhookSubscriptionsCreateSchema = z.object({
body: z.object({
name: z.string(),
url: z.url(),
url: webhookUrl,
secret: z.string().optional(),
events: z.array(z.string()),
}),
@@ -47,7 +54,7 @@ export const WebhookSubscriptionsUpdateSchema = z.object({
body: z.object({
id: z.uuid(),
name: z.string(),
url: z.url(),
url: webhookUrl,
secret: z.string().optional(),
events: z.array(z.string()),
}),
@@ -1,3 +1,4 @@
import env from "@server/env";
import {
buildAdmin,
buildUser,
@@ -167,6 +168,39 @@ describe("#webhookSubscriptions.create", () => {
expect(webhook.secret).toEqual(secret);
expect(webhook.enabled).toEqual(true);
});
it("should reject http urls when cloud hosted", async () => {
vi.spyOn(env, "isCloudHosted", "get").mockReturnValue(true);
const user = await buildAdmin();
const res = await server.post("/api/webhookSubscriptions.create", user, {
body: {
name: "Test webhook",
url: "http://www.example.com",
events: ["comments"],
},
});
expect(res.status).toEqual(400);
});
it("should allow http urls when not cloud hosted", async () => {
vi.spyOn(env, "isCloudHosted", "get").mockReturnValue(false);
const user = await buildAdmin();
const url = "http://www.example.com";
const res = await server.post("/api/webhookSubscriptions.create", user, {
body: {
name: "Test webhook",
url,
events: ["comments"],
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.url).toEqual(url);
});
});
describe("#webhookSubscriptions.update", () => {