Compare commits

...
Author SHA1 Message Date
gorkem-bwl ea698b6558 feat: add HTTP basic authentication support for monitors (#3033)
## Changes
- Add basicAuthUser and basicAuthPassword fields to Monitor model, types, and validation (server + client)
- Update networkService to send Authorization: Basic header when credentials are provided
- Add basic auth form fields (username/password) to the create/edit monitor page for HTTP monitors
- Add i18n strings for the new basic authentication section

## Benefits
- Users can monitor endpoints protected by HTTP basic authentication
- Credentials are sent as standard Authorization: Basic base64-encoded header
- Basic auth is only used when bearer token (secret) is not configured
2026-03-04 15:45:38 -05:00
9 changed files with 85 additions and 2 deletions

No files matched your search

+4
View File
@@ -40,6 +40,8 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
basicAuthUser: data?.basicAuthUser || "",
basicAuthPassword: data?.basicAuthPassword || "",
};
break;
case "ping":
@@ -113,6 +115,8 @@ export const useMonitorForm = ({
matchMethod: "",
expectedValue: "",
jsonPath: "",
basicAuthUser: "",
basicAuthPassword: "",
};
}
+44
View File
@@ -770,6 +770,50 @@ const CreateMonitorPage = () => {
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.basicAuth.title")}
subtitle={t("pages.createMonitor.form.basicAuth.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="basicAuthUser"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.createMonitor.form.basicAuth.option.username.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="basicAuthPassword"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="password"
fieldLabel={t(
"pages.createMonitor.form.basicAuth.option.password.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.advanced.title")}
+2
View File
@@ -44,6 +44,8 @@ export interface Monitor {
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
basicAuthUser?: string;
basicAuthPassword?: string;
url: string;
port?: number;
isActive: boolean;
+2
View File
@@ -38,6 +38,8 @@ const httpSchema = baseSchema.extend({
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
basicAuthUser: z.string().optional(),
basicAuthPassword: z.string().optional(),
});
// Ping monitor schema
+12
View File
@@ -501,6 +501,18 @@
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
}
},
"basicAuth": {
"description": "Configure HTTP basic authentication credentials for this monitor.",
"option": {
"username": {
"label": "Username"
},
"password": {
"label": "Password"
}
},
"title": "Basic authentication"
},
"ignoreTls": {
"description": "Configure TLS/SSL certificate validation for HTTPS connections.",
"option": {
+6
View File
@@ -93,6 +93,12 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: String,
enum: ["equal", "include", "regex", ""],
},
basicAuthUser: {
type: String,
},
basicAuthPassword: {
type: String,
},
url: {
type: String,
required: true,
@@ -296,7 +296,7 @@ class NetworkService implements INetworkService {
}
private async requestHttp<T = unknown>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue, basicAuthUser, basicAuthPassword } = monitor;
const httpResponse = this.buildStatusResponse<T>({
monitor,
overrides: {
@@ -310,8 +310,15 @@ class NetworkService implements INetworkService {
if (!url) {
throw new Error("Monitor URL is required");
}
const headers: Record<string, string> = {};
if (secret) {
headers["Authorization"] = `Bearer ${secret}`;
} else if (basicAuthUser) {
const credentials = Buffer.from(`${basicAuthUser}:${basicAuthPassword || ""}`).toString("base64");
headers["Authorization"] = `Basic ${credentials}`;
}
const config: Record<string, unknown> = {
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
headers: Object.keys(headers).length > 0 ? headers : undefined,
};
if (ignoreTlsErrors) {
+2
View File
@@ -27,6 +27,8 @@ export interface Monitor {
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
basicAuthUser?: string;
basicAuthPassword?: string;
url: string;
port?: number;
isActive: boolean;
@@ -70,6 +70,8 @@ export const createMonitorBodyValidation = z.object({
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
basicAuthUser: z.union([z.string(), z.literal("")]).optional(),
basicAuthPassword: z.union([z.string(), z.literal("")]).optional(),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
selectedDisks: z.array(z.string()).optional(),
@@ -94,6 +96,8 @@ export const editMonitorBodyValidation = z.object({
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
basicAuthUser: z.union([z.string(), z.literal("")]).optional(),
basicAuthPassword: z.union([z.string(), z.literal("")]).optional(),
port: z.number().min(1).max(65535).optional(),
cpuAlertThreshold: z.number().optional(),
memoryAlertThreshold: z.number().optional(),