Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
206c6df4aa | ||
|
|
e145068287 | ||
|
|
987ed64256 | ||
|
|
ae85af26bf | ||
|
|
023cf5782d | ||
|
|
babe5ae177 | ||
|
|
4222d39a8b | ||
|
|
fb69e29adf | ||
|
|
0d90083e63 | ||
|
|
8ab049aa6c | ||
|
|
69b6955ced | ||
|
|
04dfff4ebd | ||
|
|
13a7ae82e3 | ||
|
|
4c94a7c21e | ||
|
|
40968c370b | ||
|
|
a864bc0c05 | ||
|
|
fe242b1977 | ||
|
|
d1f1d31ea2 | ||
|
|
eacbb227c3 | ||
|
|
ec129e3604 |
No files matched your search
@@ -48,6 +48,10 @@ You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.
|
||||
|
||||
Usage instructions can be found [here](https://checkmate.so/docs).
|
||||
|
||||
## Prerequisites
|
||||
- [Docker](https://www.docker.com/) installed
|
||||
- [Git](https://git-scm.com/) installed
|
||||
|
||||
## Installation
|
||||
|
||||
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
||||
@@ -80,7 +84,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
||||
If you have any questions, suggestions or comments, you have several options:
|
||||
|
||||
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
|
||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
|
||||
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (we check here from time to time)
|
||||
|
||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
|
||||
@@ -167,7 +171,7 @@ Here's how you can contribute:
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
</a>
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
||||
[](https://star-history.com/#bluewave-labs/Checkmate&Date)
|
||||
|
||||
## Our sponsors
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export const useMonitorForm = ({
|
||||
matchMethod: data?.matchMethod || "",
|
||||
expectedValue: data?.expectedValue || "",
|
||||
jsonPath: data?.jsonPath || "",
|
||||
customUserAgent: data?.customUserAgent || "",
|
||||
};
|
||||
break;
|
||||
case "ping":
|
||||
|
||||
@@ -1,28 +1,76 @@
|
||||
import { useMemo } from "react";
|
||||
import { notificationSchema } from "@/Validation/notifications";
|
||||
import type { NotificationFormData } from "@/Validation/notifications";
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
|
||||
interface UseNotificationFormOptions {
|
||||
data?: Notification | null;
|
||||
}
|
||||
|
||||
function buildDefaults(data: Notification | null): NotificationFormData {
|
||||
if (data?.type === "matrix") {
|
||||
return {
|
||||
type: "matrix",
|
||||
notificationName: data.notificationName || "",
|
||||
homeserverUrl: data.homeserverUrl || "",
|
||||
roomId: data.roomId || "",
|
||||
accessToken: data.accessToken || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "telegram") {
|
||||
return {
|
||||
type: "telegram",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
accessToken: data.accessToken || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "slack") {
|
||||
return {
|
||||
type: "slack",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "discord") {
|
||||
return {
|
||||
type: "discord",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "webhook") {
|
||||
return {
|
||||
type: "webhook",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "pager_duty") {
|
||||
return {
|
||||
type: "pager_duty",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "teams") {
|
||||
return {
|
||||
type: "teams",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
};
|
||||
}
|
||||
// Default: email (covers both data === null and data.type === "email")
|
||||
return {
|
||||
type: "email",
|
||||
notificationName: data?.notificationName || "",
|
||||
address: data?.address || "",
|
||||
};
|
||||
}
|
||||
|
||||
export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => {
|
||||
return useMemo(() => {
|
||||
const defaults =
|
||||
data?.type === "matrix"
|
||||
? {
|
||||
type: "matrix" as const,
|
||||
notificationName: data.notificationName || "",
|
||||
homeserverUrl: data.homeserverUrl || "",
|
||||
roomId: data.roomId || "",
|
||||
accessToken: data.accessToken || "",
|
||||
}
|
||||
: {
|
||||
type: (data?.type || "email") as Exclude<Notification["type"], "matrix">,
|
||||
notificationName: data?.notificationName || "",
|
||||
address: data?.address || "",
|
||||
};
|
||||
|
||||
const defaults = buildDefaults(data);
|
||||
return { schema: notificationSchema, defaults };
|
||||
}, [data]);
|
||||
};
|
||||
@@ -40,6 +40,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
|
||||
: 80,
|
||||
},
|
||||
checkTTL: data?.checkTTL ?? 30,
|
||||
defaultUserAgent: data?.defaultUserAgent || "",
|
||||
pagespeedApiKey: "",
|
||||
systemEmailPassword: "",
|
||||
};
|
||||
|
||||
@@ -795,6 +795,40 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedType === "http" && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.userAgent.title")}
|
||||
subtitle={t("pages.createMonitor.form.userAgent.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="customUserAgent"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
type="text"
|
||||
fieldLabel={t("pages.createMonitor.form.userAgent.option.label")}
|
||||
placeholder={t("pages.createMonitor.form.userAgent.option.placeholder")}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
onChange={(e) => {
|
||||
field.onChange(
|
||||
e.target.value
|
||||
.replace(/[<>]/g, "")
|
||||
.replace(/(?:javascript|data|vbscript):/gi, "")
|
||||
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // RFC 7230 header-safe chars only
|
||||
.slice(0, 500)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedType === "http" && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.advanced.title")}
|
||||
|
||||
@@ -147,7 +147,7 @@ const NotificationsCreatePage = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{watchedType !== "matrix" && (
|
||||
{watchedType !== "matrix" && watchedType !== "telegram" && (
|
||||
<ConfigBox
|
||||
title={addressConfig.title}
|
||||
subtitle={addressConfig.description}
|
||||
@@ -155,7 +155,7 @@ const NotificationsCreatePage = () => {
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
defaultValue={defaults.address}
|
||||
defaultValue={"address" in defaults ? defaults.address : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
@@ -171,6 +171,49 @@ const NotificationsCreatePage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "telegram" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.telegram.title")}
|
||||
subtitle={t("pages.notifications.form.telegram.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="accessToken"
|
||||
control={control}
|
||||
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.telegram.optionBotToken")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.telegram.placeholderBotToken"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.telegram.optionChatId")}
|
||||
placeholder={t("pages.notifications.form.telegram.placeholderChatId")}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "matrix" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.matrix.title")}
|
||||
@@ -180,7 +223,7 @@ const NotificationsCreatePage = () => {
|
||||
<Controller
|
||||
name="homeserverUrl"
|
||||
control={control}
|
||||
defaultValue={defaults.homeserverUrl}
|
||||
defaultValue={"homeserverUrl" in defaults ? defaults.homeserverUrl : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
@@ -196,7 +239,7 @@ const NotificationsCreatePage = () => {
|
||||
<Controller
|
||||
name="roomId"
|
||||
control={control}
|
||||
defaultValue={defaults.roomId}
|
||||
defaultValue={"roomId" in defaults ? defaults.roomId : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
@@ -212,7 +255,7 @@ const NotificationsCreatePage = () => {
|
||||
<Controller
|
||||
name="accessToken"
|
||||
control={control}
|
||||
defaultValue={defaults.accessToken}
|
||||
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
|
||||
@@ -425,6 +425,39 @@ export const SettingsPage = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Default User-Agent - Admin Only */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.userAgent.title")}
|
||||
subtitle={t("pages.settings.form.userAgent.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="defaultUserAgent"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
fieldLabel={t("pages.settings.form.userAgent.option.label")}
|
||||
placeholder={t("pages.settings.form.userAgent.option.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
onChange={(e) => {
|
||||
field.onChange(
|
||||
e.target.value
|
||||
.replace(/[<>]/g, "")
|
||||
.replace(/(?:javascript|data|vbscript):/gi, "")
|
||||
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // RFC 7230 header-safe chars only
|
||||
.slice(0, 500)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Clear All Stats */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface Monitor {
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
group: string | null;
|
||||
customUserAgent?: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
geoCheckInterval?: number;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const NotificationChannels = [
|
||||
"pager_duty",
|
||||
"matrix",
|
||||
"teams",
|
||||
"telegram",
|
||||
] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Settings {
|
||||
systemEmailRequireTLS: boolean;
|
||||
systemEmailRejectUnauthorized: boolean;
|
||||
showURL: boolean;
|
||||
defaultUserAgent?: string;
|
||||
singleton: boolean;
|
||||
globalThresholds?: SettingsThresholds;
|
||||
createdAt: string;
|
||||
|
||||
@@ -38,6 +38,16 @@ const httpSchema = baseSchema.extend({
|
||||
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
|
||||
expectedValue: z.string().optional(),
|
||||
jsonPath: z.string().optional(),
|
||||
customUserAgent: z
|
||||
.string()
|
||||
.max(500)
|
||||
.regex(
|
||||
/^[\t\x20-\x7E\x80-\xFF]*$/,
|
||||
"Only printable characters, spaces, and tabs are allowed (RFC 7230 §3.2.6)"
|
||||
)
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.nullable()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// Ping monitor schema
|
||||
|
||||
@@ -50,6 +50,12 @@ const teamsSchema = baseSchema.extend({
|
||||
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
||||
});
|
||||
|
||||
const telegramSchema = baseSchema.extend({
|
||||
type: z.literal("telegram"),
|
||||
address: z.string().min(1, "Chat ID is required"),
|
||||
accessToken: z.string().min(1, "Bot token is required"),
|
||||
});
|
||||
|
||||
export const notificationSchema = z.discriminatedUnion("type", [
|
||||
emailSchema,
|
||||
slackSchema,
|
||||
@@ -58,6 +64,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
pagerDutySchema,
|
||||
matrixSchema,
|
||||
teamsSchema,
|
||||
telegramSchema,
|
||||
]);
|
||||
|
||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||
@@ -12,6 +12,15 @@ export const settingsSchema = z.object({
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
systemEmailPool: z.boolean().optional(),
|
||||
showURL: z.boolean().optional(),
|
||||
defaultUserAgent: z
|
||||
.string()
|
||||
.max(500)
|
||||
.regex(
|
||||
/^[\t\x20-\x7E\x80-\xFF]*$/,
|
||||
"Only printable characters, spaces, and tabs are allowed (RFC 7230)"
|
||||
)
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
checkTTL: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -527,6 +527,14 @@
|
||||
},
|
||||
"title": "TLS/SSL settings"
|
||||
},
|
||||
"userAgent": {
|
||||
"title": "Custom User-Agent",
|
||||
"description": "Override the User-Agent header sent with HTTP requests for this monitor. Useful for identifying Checkmate in WAF logs. Leave blank to use the global default.",
|
||||
"option": {
|
||||
"label": "User-Agent",
|
||||
"placeholder": "Checkmate/X.X (uptime monitor)"
|
||||
}
|
||||
},
|
||||
"incidents": {
|
||||
"description": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.",
|
||||
"option": {
|
||||
@@ -951,6 +959,14 @@
|
||||
"description": "Select the type of notification channel to create.",
|
||||
"optionType": "Type",
|
||||
"title": "Channel type"
|
||||
},
|
||||
"telegram": {
|
||||
"title": "Telegram configuration",
|
||||
"description": "Configure your Telegram bot to send notifications.",
|
||||
"optionBotToken": "Bot token",
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "Chat ID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1043,6 +1059,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"userAgent": {
|
||||
"title": "Default User-Agent",
|
||||
"description": "Set a default User-Agent header sent with all HTTP uptime monitor requests. Individual monitors can override this. Useful for identifying Checkmate in WAF logs and whitelists.",
|
||||
"option": {
|
||||
"label": "Default User-Agent",
|
||||
"placeholder": "Checkmate/X.X (uptime monitor)"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"title": "Monitor history",
|
||||
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
PagerDutyProvider,
|
||||
MatrixProvider,
|
||||
TeamsProvider,
|
||||
TelegramProvider,
|
||||
// Interfaces
|
||||
INetworkService,
|
||||
IEmailService,
|
||||
@@ -296,6 +297,7 @@ export const initializeServices = async ({
|
||||
const pagerDutyProvider = new PagerDutyProvider(logger);
|
||||
const matrixProvider = new MatrixProvider(logger);
|
||||
const teamsProvider = new TeamsProvider(logger);
|
||||
const telegramProvider = new TelegramProvider(logger);
|
||||
|
||||
const notificationsService = new NotificationsService(
|
||||
notificationsRepository,
|
||||
@@ -307,6 +309,7 @@ export const initializeServices = async ({
|
||||
pagerDutyProvider,
|
||||
matrixProvider,
|
||||
teamsProvider,
|
||||
telegramProvider,
|
||||
settingsService,
|
||||
logger,
|
||||
notificationMessageBuilder
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const addUserAgentFields = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
ALTER TABLE monitors
|
||||
ADD COLUMN IF NOT EXISTS custom_user_agent TEXT;
|
||||
|
||||
ALTER TABLE app_settings
|
||||
ADD COLUMN IF NOT EXISTS default_user_agent TEXT;
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropUserAgentFields = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
ALTER TABLE monitors
|
||||
DROP COLUMN IF EXISTS custom_user_agent;
|
||||
|
||||
ALTER TABLE app_settings
|
||||
DROP COLUMN IF EXISTS default_user_agent;
|
||||
`);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import { createStatusPages, dropStatusPages } from "./0018_create_status_pages.j
|
||||
import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js";
|
||||
import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js";
|
||||
import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.js";
|
||||
import { addUserAgentFields, dropUserAgentFields } from "./0022_add_user_agent_fields.js";
|
||||
|
||||
const SERVICE_NAME = "TimescaleDB Migrations";
|
||||
|
||||
@@ -52,6 +53,7 @@ const migrations: MigrationEntry[] = [
|
||||
{ name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings },
|
||||
{ name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates },
|
||||
{ name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression },
|
||||
{ name: "0022_add_user_agent_fields", up: addUserAgentFields, down: dropUserAgentFields },
|
||||
];
|
||||
|
||||
const ensureMigrationsTable = async (pool: Pool) => {
|
||||
|
||||
@@ -36,6 +36,7 @@ const AppSettingsSchema = new Schema<AppSettingsDocument>(
|
||||
systemEmailRequireTLS: { type: Boolean, default: false },
|
||||
systemEmailRejectUnauthorized: { type: Boolean, default: true },
|
||||
showURL: { type: Boolean, default: false },
|
||||
defaultUserAgent: { type: String },
|
||||
singleton: { type: Boolean, required: true, unique: true, default: true },
|
||||
version: { type: Number, default: 1 },
|
||||
globalThresholds: { type: thresholdsSchema },
|
||||
|
||||
@@ -339,6 +339,9 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
return value && value.trim() ? value.trim() : null;
|
||||
},
|
||||
},
|
||||
customUserAgent: {
|
||||
type: String,
|
||||
},
|
||||
geoCheckEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[],
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram"] as NotificationChannel[],
|
||||
required: true,
|
||||
},
|
||||
notificationName: {
|
||||
|
||||
@@ -387,6 +387,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
gameId: doc.gameId ?? undefined,
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
customUserAgent: doc.customUserAgent ?? undefined,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||
@@ -446,6 +447,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
gameId: doc.gameId ?? undefined,
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
customUserAgent: doc.customUserAgent ?? undefined,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||
|
||||
@@ -37,6 +37,7 @@ interface MonitorRow {
|
||||
game_id: string | null;
|
||||
grpc_service_name: string | null;
|
||||
monitor_group: string | null;
|
||||
custom_user_agent: string | null;
|
||||
geo_check_enabled: boolean;
|
||||
geo_check_locations: GeoContinent[] | null;
|
||||
geo_check_interval_ms: number;
|
||||
@@ -49,7 +50,7 @@ const MONITOR_COLUMNS = `id, user_id, team_id, name, description, type, status,
|
||||
interval_ms, is_active, status_window, status_window_size, status_window_threshold, uptime_percentage,
|
||||
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
|
||||
disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
|
||||
game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
|
||||
game_id, grpc_service_name, monitor_group, custom_user_agent, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
|
||||
created_at, updated_at`;
|
||||
|
||||
export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
@@ -62,8 +63,8 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
interval_ms, is_active, status_window, status_window_size, status_window_threshold,
|
||||
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
|
||||
disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
|
||||
game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms)
|
||||
VALUES ($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)
|
||||
game_id, grpc_service_name, monitor_group, custom_user_agent, geo_check_enabled, geo_check_locations, geo_check_interval_ms)
|
||||
VALUES ($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)
|
||||
RETURNING ${MONITOR_COLUMNS}`,
|
||||
[
|
||||
userId,
|
||||
@@ -97,6 +98,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
monitor.gameId ?? null,
|
||||
monitor.grpcServiceName ?? null,
|
||||
monitor.group ?? null,
|
||||
monitor.customUserAgent ?? null,
|
||||
monitor.geoCheckEnabled ?? false,
|
||||
monitor.geoCheckLocations ?? [],
|
||||
monitor.geoCheckInterval ?? 300000,
|
||||
@@ -595,6 +597,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
["gameId", "game_id"],
|
||||
["grpcServiceName", "grpc_service_name"],
|
||||
["group", "monitor_group"],
|
||||
["customUserAgent", "custom_user_agent"],
|
||||
["geoCheckEnabled", "geo_check_enabled"],
|
||||
["geoCheckLocations", "geo_check_locations"],
|
||||
["geoCheckInterval", "geo_check_interval_ms"],
|
||||
@@ -1034,6 +1037,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
gameId: row.game_id ?? undefined,
|
||||
grpcServiceName: row.grpc_service_name ?? undefined,
|
||||
group: row.monitor_group,
|
||||
customUserAgent: row.custom_user_agent ?? undefined,
|
||||
geoCheckEnabled: row.geo_check_enabled,
|
||||
geoCheckLocations: row.geo_check_locations ?? [],
|
||||
geoCheckInterval: row.geo_check_interval_ms,
|
||||
|
||||
@@ -38,6 +38,7 @@ class MongoSettingsRepository implements ISettingsRepository {
|
||||
systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false,
|
||||
systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true,
|
||||
showURL: doc.showURL ?? false,
|
||||
defaultUserAgent: doc.defaultUserAgent ?? undefined,
|
||||
singleton: doc.singleton,
|
||||
version: doc.version ?? 1,
|
||||
globalThresholds: doc.globalThresholds ?? undefined,
|
||||
|
||||
@@ -21,6 +21,7 @@ interface SettingsRow {
|
||||
system_email_require_tls: boolean | null;
|
||||
system_email_reject_unauthorized: boolean | null;
|
||||
show_url: boolean | null;
|
||||
default_user_agent: string | null;
|
||||
version: string | null;
|
||||
threshold_cpu_usage: number | null;
|
||||
threshold_memory_usage: number | null;
|
||||
@@ -34,7 +35,7 @@ const COLUMNS = `id, check_ttl, language, jwt_secret, pagespeed_api_key,
|
||||
system_email_host, system_email_port, system_email_address, system_email_password, system_email_user,
|
||||
system_email_connection_host, system_email_tls_servername, system_email_secure, system_email_pool,
|
||||
system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized,
|
||||
show_url, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature,
|
||||
show_url, default_user_agent, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature,
|
||||
created_at, updated_at`;
|
||||
|
||||
export class TimescaleSettingsRepository implements ISettingsRepository {
|
||||
@@ -46,8 +47,8 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
|
||||
system_email_host, system_email_port, system_email_address, system_email_password, system_email_user,
|
||||
system_email_connection_host, system_email_tls_servername, system_email_secure, system_email_pool,
|
||||
system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized,
|
||||
show_url, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
|
||||
show_url, default_user_agent, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23)
|
||||
RETURNING ${COLUMNS}`,
|
||||
[
|
||||
settings.checkTTL ?? 30,
|
||||
@@ -67,6 +68,7 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
|
||||
settings.systemEmailRequireTLS ?? false,
|
||||
settings.systemEmailRejectUnauthorized ?? true,
|
||||
settings.showURL ?? false,
|
||||
settings.defaultUserAgent ?? null,
|
||||
settings.version ?? 1,
|
||||
settings.globalThresholds?.cpu ?? null,
|
||||
settings.globalThresholds?.memory ?? null,
|
||||
@@ -113,6 +115,7 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
|
||||
["systemEmailRequireTLS", "system_email_require_tls"],
|
||||
["systemEmailRejectUnauthorized", "system_email_reject_unauthorized"],
|
||||
["showURL", "show_url"],
|
||||
["defaultUserAgent", "default_user_agent"],
|
||||
["version", "version"],
|
||||
];
|
||||
|
||||
@@ -197,6 +200,7 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
|
||||
systemEmailRequireTLS: row.system_email_require_tls ?? false,
|
||||
systemEmailRejectUnauthorized: row.system_email_reject_unauthorized ?? true,
|
||||
showURL: row.show_url ?? false,
|
||||
defaultUserAgent: row.default_user_agent ?? undefined,
|
||||
singleton: true,
|
||||
version: Number(row.version ?? 1),
|
||||
globalThresholds:
|
||||
|
||||
@@ -29,6 +29,7 @@ export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/teams.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/telegram.js";
|
||||
|
||||
// System services
|
||||
export * from "@/service/system/settingsService.js";
|
||||
@@ -146,7 +146,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
|
||||
deleteJob = async (monitor: Monitor) => {
|
||||
this.scheduler.removeJob(monitor.id);
|
||||
this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||
const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`);
|
||||
if (geoJob) await this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||
};
|
||||
|
||||
pauseJob = async (monitor: Monitor) => {
|
||||
@@ -154,7 +155,9 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
if (result === false) {
|
||||
throw new Error("Failed to pause monitor");
|
||||
}
|
||||
await this.scheduler.pauseJob(`${monitor.id}-geo`);
|
||||
const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`);
|
||||
if (geoJob) await this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
@@ -177,31 +180,35 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
});
|
||||
};
|
||||
|
||||
private syncGeoJob = async (monitor: Monitor) => {
|
||||
const geoJobId = `${monitor.id}-geo`;
|
||||
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||
|
||||
// If geoChecks have been disabled, or the monitor type doesn't support them, remove
|
||||
if (!monitor.geoCheckEnabled || !supportsGeoCheck(monitor.type)) {
|
||||
if (existingGeoJob) this.scheduler.removeJob(geoJobId);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the job exists, update it
|
||||
if (existingGeoJob) {
|
||||
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor });
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create it
|
||||
this.scheduler.addJob({
|
||||
id: geoJobId,
|
||||
template: "geo-check-job",
|
||||
repeat: monitor.geoCheckInterval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor: Monitor) => {
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
|
||||
// Handle geo check job lifecycle
|
||||
const geoJobId = `${monitor.id}-geo`;
|
||||
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
|
||||
// Check if geo job exists
|
||||
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||
if (existingGeoJob) {
|
||||
// Update existing geo job
|
||||
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor });
|
||||
} else {
|
||||
// Create new geo job
|
||||
this.scheduler.addJob({
|
||||
id: geoJobId,
|
||||
template: "geo-check-job",
|
||||
repeat: monitor.geoCheckInterval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Remove geo job if disabled or monitor type changed
|
||||
this.scheduler.removeJob(geoJobId);
|
||||
}
|
||||
await this.syncGeoJob(monitor);
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
|
||||
@@ -132,7 +132,15 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
}
|
||||
|
||||
// Step 2. Request monitor status
|
||||
const status = await this.networkService.requestStatus(monitor);
|
||||
// Resolve default user agent for HTTP monitors (cached, invalidated on settings update)
|
||||
let effectiveMonitor: Monitor = monitor;
|
||||
if (monitor.type === "http" && !monitor.customUserAgent) {
|
||||
const defaultUserAgent = await this.settingsService.getDefaultUserAgent();
|
||||
if (defaultUserAgent) {
|
||||
effectiveMonitor = { ...monitor, customUserAgent: defaultUserAgent };
|
||||
}
|
||||
}
|
||||
const status = await this.networkService.requestStatus(effectiveMonitor);
|
||||
if (!status) {
|
||||
throw new Error("No network response");
|
||||
}
|
||||
|
||||
@@ -56,15 +56,30 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
};
|
||||
}
|
||||
|
||||
private sanitizeHeaderValue(value: string): string {
|
||||
if (!value) return "";
|
||||
|
||||
return value
|
||||
.replace(/[<>]/g, "")
|
||||
.replace(/(?:javascript|data|vbscript):/gi, "")
|
||||
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // allow only RFC 7230 header-safe characters
|
||||
.trim()
|
||||
.slice(0, 500);
|
||||
}
|
||||
|
||||
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
|
||||
const { url, secret, jsonPath, ignoreTlsErrors } = monitor;
|
||||
const { url, secret, jsonPath, ignoreTlsErrors, customUserAgent } = monitor;
|
||||
|
||||
if (!url) {
|
||||
throw new Error("URL is required for HTTP monitor");
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
||||
if (customUserAgent) headers["User-Agent"] = this.sanitizeHeaderValue(customUserAgent);
|
||||
|
||||
const options: Record<string, unknown> = {
|
||||
headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
};
|
||||
|
||||
options.agent = {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
const SERVICE_NAME = "TelegramProvider";
|
||||
import type { Notification } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import got from "got";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
|
||||
export class TelegramProvider implements INotificationProvider {
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
if (!notification.address || !notification.accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, {
|
||||
json: {
|
||||
chat_id: notification.address,
|
||||
text: getTestMessage(),
|
||||
parse_mode: "HTML",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : "unknown error";
|
||||
const errStack = error instanceof Error ? error.stack : undefined;
|
||||
this.logger.warn({
|
||||
message: "Telegram test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: errStack,
|
||||
details: { error: errMsg },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
if (!notification.address || !notification.accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = this.buildTelegramText(message);
|
||||
|
||||
try {
|
||||
await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, {
|
||||
json: {
|
||||
chat_id: notification.address,
|
||||
text,
|
||||
parse_mode: "HTML",
|
||||
},
|
||||
});
|
||||
this.logger.info({
|
||||
message: "Telegram notification sent",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : "unknown error";
|
||||
const errStack = error instanceof Error ? error.stack : undefined;
|
||||
this.logger.warn({
|
||||
message: "Telegram alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: errStack,
|
||||
details: { error: errMsg },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private buildTelegramText(message: NotificationMessage): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`<b>${message.content.title}</b>`);
|
||||
lines.push(message.content.summary);
|
||||
lines.push("");
|
||||
|
||||
lines.push("<b>Monitor Details:</b>");
|
||||
lines.push(`• Name: ${message.monitor.name}`);
|
||||
lines.push(`• URL: ${message.monitor.url}`);
|
||||
lines.push(`• Type: ${message.monitor.type}`);
|
||||
lines.push(`• Status: ${message.monitor.status}`);
|
||||
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("<b>Additional Information:</b>");
|
||||
message.content.details.forEach((detail) => lines.push(`• ${detail}`));
|
||||
}
|
||||
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("<b>Threshold Breaches:</b>");
|
||||
message.content.thresholds.forEach((breach) => {
|
||||
lines.push(`• ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`);
|
||||
});
|
||||
}
|
||||
|
||||
if (message.content.incident) {
|
||||
lines.push("");
|
||||
lines.push(`<a href="${message.clientHost}/incidents/${message.monitor.id}">View Incident</a>`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export class NotificationsService implements INotificationsService {
|
||||
private pagerDutyProvider: INotificationProvider;
|
||||
private matrixProvider: INotificationProvider;
|
||||
private teamsProvider: INotificationProvider;
|
||||
private telegramProvider: INotificationProvider;
|
||||
private logger: ILogger;
|
||||
private settingsService: ISettingsService;
|
||||
private notificationMessageBuilder: INotificationMessageBuilder;
|
||||
@@ -47,6 +48,7 @@ export class NotificationsService implements INotificationsService {
|
||||
pagerDutyProvider: INotificationProvider,
|
||||
matrixProvider: INotificationProvider,
|
||||
teamsProvider: INotificationProvider,
|
||||
telegramProvider: INotificationProvider,
|
||||
settingsService: ISettingsService,
|
||||
logger: ILogger,
|
||||
notificationMessageBuilder: INotificationMessageBuilder
|
||||
@@ -60,6 +62,7 @@ export class NotificationsService implements INotificationsService {
|
||||
this.pagerDutyProvider = pagerDutyProvider;
|
||||
this.matrixProvider = matrixProvider;
|
||||
this.teamsProvider = teamsProvider;
|
||||
this.telegramProvider = telegramProvider;
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
this.notificationMessageBuilder = notificationMessageBuilder;
|
||||
@@ -97,6 +100,8 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.emailProvider.sendMessage!(notification, notificationMessage);
|
||||
case "teams":
|
||||
return await this.teamsProvider.sendMessage!(notification, notificationMessage);
|
||||
case "telegram":
|
||||
return await this.telegramProvider.sendMessage!(notification, notificationMessage);
|
||||
default:
|
||||
this.logger.warn({
|
||||
message: `Unknown notification type: ${notification.type}`,
|
||||
@@ -157,6 +162,8 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.webhookProvider.sendTestAlert(notification);
|
||||
case "teams":
|
||||
return await this.teamsProvider.sendTestAlert(notification);
|
||||
case "telegram":
|
||||
return await this.telegramProvider.sendTestAlert(notification);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ISettingsService {
|
||||
loadSettings(): EnvConfig;
|
||||
getSettings(): EnvConfig;
|
||||
getDBSettings(): Promise<Settings>;
|
||||
getDefaultUserAgent(): Promise<string | undefined>;
|
||||
updateDbSettings(newSettings: SettingsUpdate): Promise<Settings>;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ export class SettingsService implements ISettingsService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private settings: EnvConfig;
|
||||
private settingsRepository: ISettingsRepository | null = null;
|
||||
private cachedDefaultUserAgent: string | null | undefined = undefined;
|
||||
|
||||
constructor(env: ValidatedEnv) {
|
||||
this.settings = {
|
||||
@@ -70,8 +72,21 @@ export class SettingsService implements ISettingsService {
|
||||
return this.settingsRepository;
|
||||
}
|
||||
|
||||
getDefaultUserAgent = async (): Promise<string | undefined> => {
|
||||
if (this.cachedDefaultUserAgent !== undefined) {
|
||||
return this.cachedDefaultUserAgent ?? undefined;
|
||||
}
|
||||
const settings = await this.getDBSettings();
|
||||
this.cachedDefaultUserAgent = settings.defaultUserAgent ?? null;
|
||||
return this.cachedDefaultUserAgent ?? undefined;
|
||||
};
|
||||
|
||||
updateDbSettings = async (newSettings: SettingsUpdate) => {
|
||||
return await this.getRepository().update(newSettings);
|
||||
const updated = await this.getRepository().update(newSettings);
|
||||
if ("defaultUserAgent" in newSettings) {
|
||||
this.cachedDefaultUserAgent = newSettings.defaultUserAgent ?? null;
|
||||
}
|
||||
return updated;
|
||||
};
|
||||
|
||||
getDBSettings = async () => {
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface Monitor {
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
group: string | null;
|
||||
customUserAgent?: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
geoCheckInterval?: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as const;
|
||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram"] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
export interface Notification {
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Settings {
|
||||
systemEmailRequireTLS: boolean;
|
||||
systemEmailRejectUnauthorized: boolean;
|
||||
showURL: boolean;
|
||||
defaultUserAgent?: string;
|
||||
singleton: boolean;
|
||||
version: number;
|
||||
globalThresholds?: SettingsThresholds;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { booleanCoercion, httpHeaderValueSchema } from "./shared.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
|
||||
|
||||
@@ -75,6 +75,7 @@ export const createMonitorBodyValidation = z.object({
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
|
||||
customUserAgent: httpHeaderValueSchema.nullable().optional(),
|
||||
geoCheckEnabled: z.boolean().optional(),
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
||||
geoCheckInterval: z.number().min(300000).optional(),
|
||||
@@ -104,6 +105,7 @@ export const editMonitorBodyValidation = z.object({
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
|
||||
customUserAgent: httpHeaderValueSchema.nullable().optional(),
|
||||
geoCheckEnabled: z.boolean().optional(),
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
||||
geoCheckInterval: z.number().min(300000).optional(),
|
||||
@@ -157,6 +159,7 @@ const importedMonitorSchema = z.object({
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
|
||||
group: z.union([z.string().max(50).trim(), z.null()]).default(null),
|
||||
customUserAgent: httpHeaderValueSchema.nullable().optional(),
|
||||
geoCheckEnabled: z.boolean().default(false),
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
|
||||
geoCheckInterval: z.number().min(300000).default(300000),
|
||||
|
||||
@@ -65,6 +65,13 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
|
||||
type: z.literal("teams"),
|
||||
address: z.url({ message: "Please enter a valid Webhook URL" }),
|
||||
}),
|
||||
// Telegram notification
|
||||
z.object({
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
type: z.literal("telegram"),
|
||||
address: z.string().min(1, "Chat ID is required"),
|
||||
accessToken: z.string().min(1, "Bot token is required"),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const testNotificationBodyValidation = createNotificationBodyValidation;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CHECK_TTL_SENTINEL } from "@/types/check.js";
|
||||
import { z } from "zod";
|
||||
import { httpHeaderValueSchema } from "@/validation/shared.js";
|
||||
|
||||
//****************************************
|
||||
// Settings Validations
|
||||
@@ -20,6 +21,7 @@ export const updateAppSettingsBodyValidation = z
|
||||
systemEmailTLSServername: z.string().nullable().optional(),
|
||||
|
||||
showURL: z.boolean().optional(),
|
||||
defaultUserAgent: httpHeaderValueSchema.nullable().optional(),
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
systemEmailPool: z.boolean().optional(),
|
||||
systemEmailIgnoreTLS: z.boolean().optional(),
|
||||
|
||||
@@ -21,6 +21,12 @@ export const booleanCoercion = z.preprocess((val) => {
|
||||
return val; // Let Zod validation handle invalid values
|
||||
}, z.boolean());
|
||||
|
||||
// RFC 7230: validate HTTP header field-value characters
|
||||
export const httpHeaderValueSchema = z
|
||||
.string()
|
||||
.max(500)
|
||||
.regex(/^[\t\x20-\x7E\x80-\xFF]*$/, "Must contain only valid HTTP header characters (RFC 7230)");
|
||||
|
||||
export const roleValidator = (allowedRoles: UserRole[]) => {
|
||||
return z.array(z.custom<UserRole>()).refine((userRoles) => allowedRoles.some((role) => userRoles.includes(role)), {
|
||||
message: `You do not have the required authorization. Required roles: ${allowedRoles.join(", ")}`,
|
||||
|
||||
Reference in new issue
Block a user