Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/src/Hooks/useNotificationForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ function buildDefaults(data: Notification | null): NotificationFormData {
type: "webhook",
notificationName: data.notificationName || "",
address: data.address || "",
webhookAuthType: data.webhookAuthType || "none",
webhookUsername: data.webhookUsername || "",
webhookPassword: data.webhookPassword || "",
webhookToken: data.webhookToken || "",
};
}
if (data?.type === "pager_duty") {
Expand Down
86 changes: 86 additions & 0 deletions client/src/Pages/Notifications/create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,92 @@ const NotificationsCreatePage = () => {
}
/>
)}
{watchedType === "webhook" && (
<ConfigBox
title={t("pages.notifications.form.webhookAuth.title")}
subtitle={t("pages.notifications.form.webhookAuth.description")}
rightContent={
<Stack spacing={theme.spacing(8)}>
<Controller
name="webhookAuthType"
control={control}
defaultValue={"webhookAuthType" in defaults ? defaults.webhookAuthType : "none"}
render={({ field }) => (
<Select
value={field.value}
fieldLabel={t("pages.notifications.form.webhookAuth.optionAuthType")}
onChange={field.onChange}
>
<MenuItem value="none">
<Typography>{t("pages.notifications.form.webhookAuth.authNone")}</Typography>
</MenuItem>
<MenuItem value="basic">
<Typography>{t("pages.notifications.form.webhookAuth.authBasic")}</Typography>
</MenuItem>
<MenuItem value="bearer">
<Typography>{t("pages.notifications.form.webhookAuth.authBearer")}</Typography>
</MenuItem>
</Select>
)}
/>
{watch("webhookAuthType") === "basic" && (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's hoist this like the other watch calls, this shouldn't really be inline. It's very easy to miss that this is even here

<>
<Controller
name="webhookUsername"
control={control}
defaultValue={"webhookUsername" in defaults ? defaults.webhookUsername : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.webhookAuth.optionUsername")}
placeholder={t("pages.notifications.form.webhookAuth.placeholderUsername")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="webhookPassword"
control={control}
defaultValue={"webhookPassword" in defaults ? defaults.webhookPassword : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="password"
fieldLabel={t("pages.notifications.form.webhookAuth.optionPassword")}
placeholder={t("pages.notifications.form.webhookAuth.placeholderPassword")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</>
)}
{watch("webhookAuthType") === "bearer" && (
<Controller
name="webhookToken"
control={control}
defaultValue={"webhookToken" in defaults ? defaults.webhookToken : ""}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.notifications.form.webhookAuth.optionToken")}
placeholder={t("pages.notifications.form.webhookAuth.placeholderToken")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
</Stack>
}
/>
)}
{watchedType === "ntfy" && (
<ConfigBox
title={t("pages.notifications.form.ntfy.title")}
Expand Down
4 changes: 4 additions & 0 deletions client/src/Types/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export interface Notification {
accountSid?: string;
twilioPhoneNumber?: string;
topic?: string;
webhookAuthType?: 'none' | 'basic' | 'bearer';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should not be inlined as it requires duplicate maintenance whenever this is updated, both here and in the validation schema.

This should be declared and exported as all other enums in the applicaiton:

export const WebhookAuthTypes = ["none", "basic", "bearer"] as const;
export type WebhookAuthType = (typeof WebhookAuthTypes)[number];

webhookUsername?: string;
webhookPassword?: string;
webhookToken?: string;
createdAt: string;
updatedAt: string;
}
4 changes: 4 additions & 0 deletions client/src/Validation/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const discordSchema = baseSchema.extend({
const webhookSchema = baseSchema.extend({
type: z.literal("webhook"),
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
webhookAuthType: z.enum(["none", "basic", "bearer"]).optional(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline enum duplication here as mentioned in notification.ts

webhookUsername: z.string().optional(),
webhookPassword: z.string().optional(),
webhookToken: z.string().optional(),
});

const pagerDutySchema = baseSchema.extend({
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,20 @@
"placeholderServerUrl": "https://ntfy.sh",
"optionTopic": "Topic",
"placeholderTopic": "checkmate-alerts"
},
"webhookAuth": {
"title": "Webhook authentication",
"description": "Optionally configure authentication for your webhook requests.",
"optionAuthType": "Authentication type",
"authNone": "None",
"authBasic": "Basic Auth",
"authBearer": "Bearer Token",
"optionUsername": "Username",
"placeholderUsername": "Enter username",
"optionPassword": "Password",
"placeholderPassword": "Enter password",
"optionToken": "Token",
"placeholderToken": "Enter bearer token"
}
},
"table": {
Expand Down
4 changes: 4 additions & 0 deletions server/src/api/validation/notificationValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
roomId: z.union([z.string(), z.literal("")]).optional(),
accessToken: z.union([z.string(), z.literal("")]).optional(),
webhookAuthType: z.enum(["none", "basic", "bearer"]).optional(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There needs to be a super refine applied here to properly validate webhook authentication.

For example, if I select basic with an empty username/password it passes validation on both client and server, but is clearly not a valid basic auth schema.

{
  "_id": {
    "$oid": "6a5e5048a3a78419627a835e"
  },
  "userId": {
    "$oid": "6a25a150a2c194721064fb15"
  },
  "teamId": {
    "$oid": "6a25a150a2c194721064fb13"
  },
  "type": "webhook",
  "notificationName": "test",
  "address": "https://www.google.ca",
  "webhookAuthType": "basic",
  "webhookUsername": "",
  "webhookPassword": "",
  "createdAt": {
    "$date": "2026-07-20T16:43:52.067Z"
  },
  "updatedAt": {
    "$date": "2026-07-20T16:43:52.067Z"
  },
  "__v": 0
}

I was able to create this, which I should not be able to.

webhookUsername: z.union([z.string(), z.literal("")]).optional(),
webhookPassword: z.union([z.string(), z.literal("")]).optional(),
webhookToken: z.union([z.string(), z.literal("")]).optional(),
}),
// Slack notification
z.object({
Expand Down
4 changes: 4 additions & 0 deletions server/src/domain/notifications/notification.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ const NotificationSchema = new Schema<NotificationDocument>(
accountSid: { type: String },
twilioPhoneNumber: { type: String },
topic: { type: String },
webhookAuthType: { type: String, enum: ['none', 'basic', 'bearer'] },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is failing formatting

webhookUsername: { type: String },
webhookPassword: { type: String },
webhookToken: { type: String },
},
{
timestamps: true,
Expand Down
4 changes: 4 additions & 0 deletions server/src/domain/notifications/notification.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export interface Notification {
accountSid?: string;
twilioPhoneNumber?: string;
topic?: string;
webhookAuthType?: 'none' | 'basic' | 'bearer';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, format failure

webhookUsername?: string;
webhookPassword?: string;
webhookToken?: string;
createdAt: string;
updatedAt: string;
}
Expand Down
21 changes: 21 additions & 0 deletions server/src/domain/notifications/providers/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ import { getTestMessage } from "@/domain/notifications/providers/utils.js";
import got from "got";

export class WebhookProvider extends NotificationProvider {
/**
* Build authorization header based on webhook auth configuration
*/
private buildAuthHeaders = (notification: Notification): Record<string, string> => {
const headers: Record<string, string> = {};

if (notification.webhookAuthType === "basic") {
const username = notification.webhookUsername || "";
const password = notification.webhookPassword || "";
const encoded = Buffer.from(username + ":" + password).toString("base64");
headers["Authorization"] = "Basic " + encoded;
} else if (notification.webhookAuthType === "bearer") {
const token = notification.webhookToken || "";
headers["Authorization"] = "Bearer " + token;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tests

return headers;
};

sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
if (!notification.address) {
return false;
Expand All @@ -19,6 +38,7 @@ export class WebhookProvider extends NotificationProvider {
json: payload,
headers: {
"Content-Type": "application/json",
...this.buildAuthHeaders(notification),
},
...this.gotRequestOptions(),
});
Expand Down Expand Up @@ -101,6 +121,7 @@ export class WebhookProvider extends NotificationProvider {
json: { text: getTestMessage() },
headers: {
"Content-Type": "application/json",
...this.buildAuthHeaders(notification as Notification),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No casting please, we should not have to lie to the compiler in order for the code to compile. Everything should always be properly typed.

},
...this.gotRequestOptions(),
});
Expand Down
Loading