Compare commits

..
33 changed files with 183 additions and 215 deletions

No files matched your search

@@ -1,12 +1,13 @@
import type { Monitor } from "@/Types/Monitor"; import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { PulseDot, Dot } from "@/Components/design-elements"; import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils"; import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms"; import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/Palette"; import { typographyLevels } from "@/Utils/Theme/Palette";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme(); const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isSmall = useMediaQuery(theme.breakpoints.down("md"));
@@ -28,7 +29,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
<Stack <Stack
direction="row" direction="row"
alignItems={"center"} alignItems={"center"}
gap={theme.spacing(4)} gap={theme.spacing(LAYOUT.XS)}
> >
<PulseDot color={getStatusColor(monitor.status, theme)} /> <PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography <Typography
@@ -41,6 +42,12 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
> >
{formatUrl(monitor?.url)} {formatUrl(monitor?.url)}
</Typography> </Typography>
{monitor.type === "pagespeed" && monitor.strategy && (
<>
<Dot />
<StrategyBadge strategy={monitor.strategy} />
</>
)}
{!isSmall && ( {!isSmall && (
<> <>
<Dot /> <Dot />
@@ -0,0 +1,32 @@
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import type { PageSpeedStrategy } from "@/Types/Monitor";
import { SPACING } from "@/Utils/Theme/constants";
interface StrategyBadgeProps {
strategy: PageSpeedStrategy | undefined;
variant?: "caption" | "body2";
}
export const StrategyBadge = ({ strategy, variant = "body2" }: StrategyBadgeProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (!strategy) {
return null;
}
return (
<Typography
variant={variant}
px={SPACING.XXL}
py={SPACING.SM}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.secondary.light}
color={theme.palette.text.primary}
>
{t(`pages.pageSpeed.strategy.${strategy}`)}
</Typography>
);
};
@@ -21,3 +21,4 @@ export * from "./SplitBox";
export * from "./TextLink"; export * from "./TextLink";
export * from "./OfflineBanner"; export * from "./OfflineBanner";
export * from "./Avatar"; export * from "./Avatar";
export * from "./StrategyBadge";
+2 -1
View File
@@ -1,5 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor"; import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor"; import type { Monitor, MonitorType } from "@/Types/Monitor";
interface UseMonitorFormOptions { interface UseMonitorFormOptions {
@@ -40,7 +41,6 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "", matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "", expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "", jsonPath: data?.jsonPath || "",
customUserAgent: data?.customUserAgent || "",
}; };
break; break;
case "ping": case "ping":
@@ -89,6 +89,7 @@ export const useMonitorForm = ({
...base, ...base,
type: "pagespeed", type: "pagespeed",
url: data?.url || "", url: data?.url || "",
strategy: data?.strategy ?? DefaultPageSpeedStrategy,
}; };
break; break;
case "hardware": case "hardware":
-1
View File
@@ -40,7 +40,6 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
: 80, : 80,
}, },
checkTTL: data?.checkTTL ?? 30, checkTTL: data?.checkTTL ?? 30,
defaultUserAgent: data?.defaultUserAgent || "",
pagespeedApiKey: "", pagespeedApiKey: "",
systemEmailPassword: "", systemEmailPassword: "",
}; };
+36 -34
View File
@@ -37,6 +37,7 @@ import {
type MonitorType, type MonitorType,
type GamesMap, type GamesMap,
supportsGeoCheck, supportsGeoCheck,
DefaultPageSpeedStrategy,
} from "@/Types/Monitor"; } from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification"; import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor"; import type { MonitorFormData } from "@/Validation/monitor";
@@ -51,6 +52,7 @@ interface GeneralSettingsConfig {
showSecret: boolean; showSecret: boolean;
showGrpcServiceName: boolean; showGrpcServiceName: boolean;
showIgnoreTls: boolean; showIgnoreTls: boolean;
showStrategy: boolean;
} }
const getGeneralSettingsConfig = ( const getGeneralSettingsConfig = (
@@ -68,6 +70,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
ping: { ping: {
urlLabel: t("pages.createMonitor.form.general.option.host.label"), urlLabel: t("pages.createMonitor.form.general.option.host.label"),
@@ -79,6 +82,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
docker: { docker: {
urlLabel: t("pages.createMonitor.form.general.option.container.label"), urlLabel: t("pages.createMonitor.form.general.option.container.label"),
@@ -90,6 +94,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
port: { port: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -101,6 +106,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
game: { game: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -112,6 +118,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
grpc: { grpc: {
urlLabel: t("pages.createMonitor.form.general.option.host.label"), urlLabel: t("pages.createMonitor.form.general.option.host.label"),
@@ -123,6 +130,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: true, showGrpcServiceName: true,
showIgnoreTls: true, showIgnoreTls: true,
showStrategy: false,
}, },
pagespeed: { pagespeed: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -134,6 +142,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: true,
}, },
hardware: { hardware: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -145,6 +154,7 @@ const getGeneralSettingsConfig = (
showSecret: true, showSecret: true,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
websocket: { websocket: {
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"), urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
@@ -156,6 +166,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: true, showIgnoreTls: true,
showStrategy: false,
}, },
}; };
return configs[type] || configs.http; return configs[type] || configs.http;
@@ -382,6 +393,31 @@ const CreateMonitorPage = () => {
/> />
)} )}
{/* Strategy field - only for pagespeed type */}
{generalSettingsConfig.showStrategy && (
<Controller
name="strategy"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? DefaultPageSpeedStrategy}
fieldLabel={t(
"pages.createMonitor.form.general.option.strategy.label"
)}
error={!!fieldState.error}
>
<MenuItem value="desktop">
{t("pages.createMonitor.form.general.option.strategy.desktop")}
</MenuItem>
<MenuItem value="mobile">
{t("pages.createMonitor.form.general.option.strategy.mobile")}
</MenuItem>
</Select>
)}
/>
)}
{/* Port field - only for port and game types */} {/* Port field - only for port and game types */}
{generalSettingsConfig.showPort && ( {generalSettingsConfig.showPort && (
<Controller <Controller
@@ -795,40 +831,6 @@ 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" && ( {watchedType === "http" && (
<ConfigBox <ConfigBox
title={t("pages.createMonitor.form.advanced.title")} title={t("pages.createMonitor.form.advanced.title")}
@@ -1,7 +1,13 @@
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Table, Pagination, StatusLabel } from "@/Components/design-elements"; import {
Table,
Pagination,
StatusLabel,
StrategyBadge,
} from "@/Components/design-elements";
import { LAYOUT } from "@/Utils/Theme/constants";
import { HistogramPageSpeed } from "@/Components/monitors"; import { HistogramPageSpeed } from "@/Components/monitors";
import { ArrowDown, ArrowUp } from "lucide-react"; import { ArrowDown, ArrowUp } from "lucide-react";
import type { Header } from "@/Components/design-elements/Table"; import type { Header } from "@/Components/design-elements/Table";
@@ -179,7 +185,19 @@ export const PageSpeedMonitorsTable = ({
</Stack> </Stack>
), ),
render: (row) => { render: (row) => {
return row.name; return (
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XS}
>
<Typography>{row.name}</Typography>
<StrategyBadge
strategy={row.strategy}
variant="caption"
/>
</Stack>
);
}, },
}, },
{ {
-33
View File
@@ -425,39 +425,6 @@ 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 */} {/* Clear All Stats */}
{isAdmin && ( {isAdmin && (
<ConfigBox <ConfigBox
+5 -1
View File
@@ -38,6 +38,10 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
export interface Monitor { export interface Monitor {
id: string; id: string;
userId: string; userId: string;
@@ -72,8 +76,8 @@ export interface Monitor {
selectedDisks: string[]; selectedDisks: string[];
gameId?: string; gameId?: string;
grpcServiceName?: string; grpcServiceName?: string;
strategy?: PageSpeedStrategy;
group: string | null; group: string | null;
customUserAgent?: string | null;
geoCheckEnabled?: boolean; geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[]; geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number; geoCheckInterval?: number;
-1
View File
@@ -21,7 +21,6 @@ export interface Settings {
systemEmailRequireTLS: boolean; systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean; systemEmailRejectUnauthorized: boolean;
showURL: boolean; showURL: boolean;
defaultUserAgent?: string;
singleton: boolean; singleton: boolean;
globalThresholds?: SettingsThresholds; globalThresholds?: SettingsThresholds;
createdAt: string; createdAt: string;
+2 -10
View File
@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { GeoContinents } from "@/Types/GeoCheck"; import { GeoContinents } from "@/Types/GeoCheck";
import { PageSpeedStrategies } from "@/Types/Monitor";
// URL schema with custom error message // URL schema with custom error message
const urlSchema = z.url({ message: "Please enter a valid URL" }); const urlSchema = z.url({ message: "Please enter a valid URL" });
@@ -38,16 +39,6 @@ const httpSchema = baseSchema.extend({
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(), matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(), expectedValue: z.string().optional(),
jsonPath: 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 // Ping monitor schema
@@ -99,6 +90,7 @@ const grpcSchema = baseSchema.extend({
const pagespeedSchema = baseSchema.extend({ const pagespeedSchema = baseSchema.extend({
type: z.literal("pagespeed"), type: z.literal("pagespeed"),
url: urlSchema, url: urlSchema,
strategy: z.enum(PageSpeedStrategies),
}); });
// Hardware/Infrastructure monitor schema // Hardware/Infrastructure monitor schema
-9
View File
@@ -12,15 +12,6 @@ export const settingsSchema = z.object({
systemEmailSecure: z.boolean().optional(), systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(), systemEmailPool: z.boolean().optional(),
showURL: 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 checkTTL: z
.number() .number()
.int() .int()
+9 -16
View File
@@ -503,6 +503,11 @@
"wsUrl": { "wsUrl": {
"label": "WebSocket URL", "label": "WebSocket URL",
"placeholder": "wss://example.com/socket" "placeholder": "wss://example.com/socket"
},
"strategy": {
"label": "Strategy",
"desktop": "Desktop",
"mobile": "Mobile"
} }
}, },
"title": "General settings", "title": "General settings",
@@ -527,14 +532,6 @@
}, },
"title": "TLS/SSL settings" "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": { "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.", "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": { "option": {
@@ -976,6 +973,10 @@
} }
}, },
"pageSpeed": { "pageSpeed": {
"strategy": {
"desktop": "Desktop",
"mobile": "Mobile"
},
"charts": { "charts": {
"common": { "common": {
"cls": "Cumulative Layout Shift (CLS)", "cls": "Cumulative Layout Shift (CLS)",
@@ -1059,14 +1060,6 @@
} }
} }
}, },
"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": { "stats": {
"title": "Monitor history", "title": "Monitor history",
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.", "description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
+11
View File
@@ -3631,6 +3631,12 @@
"type": "string", "type": "string",
"default": "" "default": ""
}, },
"strategy": {
"type": "string",
"enum": ["desktop", "mobile"],
"default": "desktop",
"description": "PageSpeed analysis strategy"
},
"selectedDisks": { "selectedDisks": {
"type": "array", "type": "array",
"items": { "items": {
@@ -3737,6 +3743,11 @@
"grpcServiceName": { "grpcServiceName": {
"type": "string" "type": "string"
}, },
"strategy": {
"type": "string",
"enum": ["desktop", "mobile"],
"description": "PageSpeed analysis strategy"
},
"selectedDisks": { "selectedDisks": {
"type": "array", "type": "array",
"items": { "items": {
@@ -0,0 +1,15 @@
import type { Pool } from "pg";
export const addStrategyToMonitors = async (pool: Pool) => {
await pool.query(`
ALTER TABLE monitors
ADD COLUMN IF NOT EXISTS strategy TEXT DEFAULT NULL;
`);
};
export const dropStrategyFromMonitors = async (pool: Pool) => {
await pool.query(`
ALTER TABLE monitors
DROP COLUMN IF EXISTS strategy;
`);
};
@@ -1,21 +0,0 @@
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;
`);
};
+2 -2
View File
@@ -21,7 +21,7 @@ import { createStatusPages, dropStatusPages } from "./0018_create_status_pages.j
import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js"; import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js";
import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js"; import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js";
import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.js"; import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.js";
import { addUserAgentFields, dropUserAgentFields } from "./0022_add_user_agent_fields.js"; import { addStrategyToMonitors, dropStrategyFromMonitors } from "./0022_add_strategy_to_monitors.js";
const SERVICE_NAME = "TimescaleDB Migrations"; const SERVICE_NAME = "TimescaleDB Migrations";
@@ -53,7 +53,7 @@ const migrations: MigrationEntry[] = [
{ name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings }, { name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings },
{ name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates }, { name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates },
{ name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression }, { name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression },
{ name: "0022_add_user_agent_fields", up: addUserAgentFields, down: dropUserAgentFields }, { name: "0022_add_strategy_to_monitors", up: addStrategyToMonitors, down: dropStrategyFromMonitors },
]; ];
const ensureMigrationsTable = async (pool: Pool) => { const ensureMigrationsTable = async (pool: Pool) => {
-1
View File
@@ -36,7 +36,6 @@ const AppSettingsSchema = new Schema<AppSettingsDocument>(
systemEmailRequireTLS: { type: Boolean, default: false }, systemEmailRequireTLS: { type: Boolean, default: false },
systemEmailRejectUnauthorized: { type: Boolean, default: true }, systemEmailRejectUnauthorized: { type: Boolean, default: true },
showURL: { type: Boolean, default: false }, showURL: { type: Boolean, default: false },
defaultUserAgent: { type: String },
singleton: { type: Boolean, required: true, unique: true, default: true }, singleton: { type: Boolean, required: true, unique: true, default: true },
version: { type: Number, default: 1 }, version: { type: Number, default: 1 },
globalThresholds: { type: thresholdsSchema }, globalThresholds: { type: thresholdsSchema },
+5 -4
View File
@@ -1,6 +1,6 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; import { MonitorTypes, MonitorStatuses, PageSpeedStrategies } from "@/types/monitor.js";
import type { import type {
CheckAudits, CheckAudits,
CheckCaptureInfo, CheckCaptureInfo,
@@ -330,6 +330,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: String, type: String,
default: "", default: "",
}, },
strategy: {
type: String,
enum: PageSpeedStrategies,
},
group: { group: {
type: String, type: String,
trim: true, trim: true,
@@ -339,9 +343,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
return value && value.trim() ? value.trim() : null; return value && value.trim() ? value.trim() : null;
}, },
}, },
customUserAgent: {
type: String,
},
geoCheckEnabled: { geoCheckEnabled: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -386,8 +386,8 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [], selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
strategy: doc.strategy ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
customUserAgent: doc.customUserAgent ?? undefined,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
@@ -446,8 +446,8 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [], selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
strategy: doc.strategy ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
customUserAgent: doc.customUserAgent ?? undefined,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
@@ -1,5 +1,6 @@
import type { Pool } from "pg"; import type { Pool } from "pg";
import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent } from "@/types/monitor.js"; import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent, PageSpeedStrategy } from "@/types/monitor.js";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
@@ -36,8 +37,8 @@ interface MonitorRow {
selected_disks: string[] | null; selected_disks: string[] | null;
game_id: string | null; game_id: string | null;
grpc_service_name: string | null; grpc_service_name: string | null;
strategy: PageSpeedStrategy | null;
monitor_group: string | null; monitor_group: string | null;
custom_user_agent: string | null;
geo_check_enabled: boolean; geo_check_enabled: boolean;
geo_check_locations: GeoContinent[] | null; geo_check_locations: GeoContinent[] | null;
geo_check_interval_ms: number; geo_check_interval_ms: number;
@@ -50,7 +51,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, 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, 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, disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
game_id, grpc_service_name, monitor_group, custom_user_agent, geo_check_enabled, geo_check_locations, geo_check_interval_ms, game_id, grpc_service_name, strategy, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
created_at, updated_at`; created_at, updated_at`;
export class TimescaleMonitorsRepository implements IMonitorsRepository { export class TimescaleMonitorsRepository implements IMonitorsRepository {
@@ -63,7 +64,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
interval_ms, is_active, status_window, status_window_size, status_window_threshold, interval_ms, is_active, status_window, status_window_size, status_window_threshold,
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter, 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, disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
game_id, grpc_service_name, monitor_group, custom_user_agent, geo_check_enabled, geo_check_locations, geo_check_interval_ms) game_id, grpc_service_name, strategy, 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,$35) 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}`, RETURNING ${MONITOR_COLUMNS}`,
[ [
@@ -97,8 +98,8 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
monitor.selectedDisks ?? [], monitor.selectedDisks ?? [],
monitor.gameId ?? null, monitor.gameId ?? null,
monitor.grpcServiceName ?? null, monitor.grpcServiceName ?? null,
monitor.strategy ?? null,
monitor.group ?? null, monitor.group ?? null,
monitor.customUserAgent ?? null,
monitor.geoCheckEnabled ?? false, monitor.geoCheckEnabled ?? false,
monitor.geoCheckLocations ?? [], monitor.geoCheckLocations ?? [],
monitor.geoCheckInterval ?? 300000, monitor.geoCheckInterval ?? 300000,
@@ -596,8 +597,8 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
["selectedDisks", "selected_disks"], ["selectedDisks", "selected_disks"],
["gameId", "game_id"], ["gameId", "game_id"],
["grpcServiceName", "grpc_service_name"], ["grpcServiceName", "grpc_service_name"],
["strategy", "strategy"],
["group", "monitor_group"], ["group", "monitor_group"],
["customUserAgent", "custom_user_agent"],
["geoCheckEnabled", "geo_check_enabled"], ["geoCheckEnabled", "geo_check_enabled"],
["geoCheckLocations", "geo_check_locations"], ["geoCheckLocations", "geo_check_locations"],
["geoCheckInterval", "geo_check_interval_ms"], ["geoCheckInterval", "geo_check_interval_ms"],
@@ -1036,8 +1037,8 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
selectedDisks: row.selected_disks ?? [], selectedDisks: row.selected_disks ?? [],
gameId: row.game_id ?? undefined, gameId: row.game_id ?? undefined,
grpcServiceName: row.grpc_service_name ?? undefined, grpcServiceName: row.grpc_service_name ?? undefined,
strategy: row.strategy ?? undefined,
group: row.monitor_group, group: row.monitor_group,
customUserAgent: row.custom_user_agent ?? undefined,
geoCheckEnabled: row.geo_check_enabled, geoCheckEnabled: row.geo_check_enabled,
geoCheckLocations: row.geo_check_locations ?? [], geoCheckLocations: row.geo_check_locations ?? [],
geoCheckInterval: row.geo_check_interval_ms, geoCheckInterval: row.geo_check_interval_ms,
@@ -38,7 +38,6 @@ class MongoSettingsRepository implements ISettingsRepository {
systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false, systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false,
systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true, systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true,
showURL: doc.showURL ?? false, showURL: doc.showURL ?? false,
defaultUserAgent: doc.defaultUserAgent ?? undefined,
singleton: doc.singleton, singleton: doc.singleton,
version: doc.version ?? 1, version: doc.version ?? 1,
globalThresholds: doc.globalThresholds ?? undefined, globalThresholds: doc.globalThresholds ?? undefined,
@@ -21,7 +21,6 @@ interface SettingsRow {
system_email_require_tls: boolean | null; system_email_require_tls: boolean | null;
system_email_reject_unauthorized: boolean | null; system_email_reject_unauthorized: boolean | null;
show_url: boolean | null; show_url: boolean | null;
default_user_agent: string | null;
version: string | null; version: string | null;
threshold_cpu_usage: number | null; threshold_cpu_usage: number | null;
threshold_memory_usage: number | null; threshold_memory_usage: number | null;
@@ -35,7 +34,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_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_connection_host, system_email_tls_servername, system_email_secure, system_email_pool,
system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized, system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized,
show_url, default_user_agent, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature, show_url, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature,
created_at, updated_at`; created_at, updated_at`;
export class TimescaleSettingsRepository implements ISettingsRepository { export class TimescaleSettingsRepository implements ISettingsRepository {
@@ -47,8 +46,8 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
system_email_host, system_email_port, system_email_address, system_email_password, system_email_user, 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_connection_host, system_email_tls_servername, system_email_secure, system_email_pool,
system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized, system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized,
show_url, default_user_agent, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature) 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,$23) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
RETURNING ${COLUMNS}`, RETURNING ${COLUMNS}`,
[ [
settings.checkTTL ?? 30, settings.checkTTL ?? 30,
@@ -68,7 +67,6 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
settings.systemEmailRequireTLS ?? false, settings.systemEmailRequireTLS ?? false,
settings.systemEmailRejectUnauthorized ?? true, settings.systemEmailRejectUnauthorized ?? true,
settings.showURL ?? false, settings.showURL ?? false,
settings.defaultUserAgent ?? null,
settings.version ?? 1, settings.version ?? 1,
settings.globalThresholds?.cpu ?? null, settings.globalThresholds?.cpu ?? null,
settings.globalThresholds?.memory ?? null, settings.globalThresholds?.memory ?? null,
@@ -115,7 +113,6 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
["systemEmailRequireTLS", "system_email_require_tls"], ["systemEmailRequireTLS", "system_email_require_tls"],
["systemEmailRejectUnauthorized", "system_email_reject_unauthorized"], ["systemEmailRejectUnauthorized", "system_email_reject_unauthorized"],
["showURL", "show_url"], ["showURL", "show_url"],
["defaultUserAgent", "default_user_agent"],
["version", "version"], ["version", "version"],
]; ];
@@ -200,7 +197,6 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
systemEmailRequireTLS: row.system_email_require_tls ?? false, systemEmailRequireTLS: row.system_email_require_tls ?? false,
systemEmailRejectUnauthorized: row.system_email_reject_unauthorized ?? true, systemEmailRejectUnauthorized: row.system_email_reject_unauthorized ?? true,
showURL: row.show_url ?? false, showURL: row.show_url ?? false,
defaultUserAgent: row.default_user_agent ?? undefined,
singleton: true, singleton: true,
version: Number(row.version ?? 1), version: Number(row.version ?? 1),
globalThresholds: globalThresholds:
@@ -132,15 +132,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
} }
// Step 2. Request monitor status // Step 2. Request monitor status
// Resolve default user agent for HTTP monitors (cached, invalidated on settings update) const status = await this.networkService.requestStatus(monitor);
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) { if (!status) {
throw new Error("No network response"); throw new Error("No network response");
} }
@@ -56,30 +56,15 @@ 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>> { async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
const { url, secret, jsonPath, ignoreTlsErrors, customUserAgent } = monitor; const { url, secret, jsonPath, ignoreTlsErrors } = monitor;
if (!url) { if (!url) {
throw new Error("URL is required for HTTP monitor"); 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> = { const options: Record<string, unknown> = {
headers: Object.keys(headers).length > 0 ? headers : undefined, headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
}; };
options.agent = { options.agent = {
@@ -1,6 +1,6 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js"; import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js"; import { Monitor, MonitorType, DefaultPageSpeedStrategy } from "@/types/monitor.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { ISettingsService } from "@/service/system/settingsService.js"; import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
@@ -19,14 +19,15 @@ export class PageSpeedProvider implements IStatusProvider<PageSpeedStatusPayload
} }
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> { async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
const { url } = monitor; const { url, strategy } = monitor;
try { try {
if (!url) throw new Error("URL is required for PageSpeed monitor"); if (!url) throw new Error("URL is required for PageSpeed monitor");
const dbSettings = await this.settingsService.getDBSettings(); const dbSettings = await this.settingsService.getDBSettings();
const apiKey = dbSettings?.pagespeedApiKey; const apiKey = dbSettings?.pagespeedApiKey;
const resolvedStrategy = strategy ?? DefaultPageSpeedStrategy;
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent( let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
url url
)}&category=seo&category=accessibility&category=best-practices&category=performance`; )}&strategy=${encodeURIComponent(resolvedStrategy)}&category=seo&category=accessibility&category=best-practices&category=performance`;
if (apiKey) { if (apiKey) {
pageSpeedUrl += `&key=${apiKey}`; pageSpeedUrl += `&key=${apiKey}`;
@@ -249,6 +249,9 @@ export class StatusService implements IStatusService {
}; };
} }
// With a full window, a single raw check must not change monitor.status; only the sliding-window threshold can trigger a transition.
newStatus = monitor.status;
// Check if threshold has been met // Check if threshold has been met
const failures = monitor.statusWindow.filter((s) => s === false).length; const failures = monitor.statusWindow.filter((s) => s === false).length;
const failureRate = (failures / monitor.statusWindow.length) * 100; const failureRate = (failures / monitor.statusWindow.length) * 100;
+1 -16
View File
@@ -20,7 +20,6 @@ export interface ISettingsService {
loadSettings(): EnvConfig; loadSettings(): EnvConfig;
getSettings(): EnvConfig; getSettings(): EnvConfig;
getDBSettings(): Promise<Settings>; getDBSettings(): Promise<Settings>;
getDefaultUserAgent(): Promise<string | undefined>;
updateDbSettings(newSettings: SettingsUpdate): Promise<Settings>; updateDbSettings(newSettings: SettingsUpdate): Promise<Settings>;
} }
@@ -28,7 +27,6 @@ export class SettingsService implements ISettingsService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private settings: EnvConfig; private settings: EnvConfig;
private settingsRepository: ISettingsRepository | null = null; private settingsRepository: ISettingsRepository | null = null;
private cachedDefaultUserAgent: string | null | undefined = undefined;
constructor(env: ValidatedEnv) { constructor(env: ValidatedEnv) {
this.settings = { this.settings = {
@@ -72,21 +70,8 @@ export class SettingsService implements ISettingsService {
return this.settingsRepository; 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) => { updateDbSettings = async (newSettings: SettingsUpdate) => {
const updated = await this.getRepository().update(newSettings); return await this.getRepository().update(newSettings);
if ("defaultUserAgent" in newSettings) {
this.cachedDefaultUserAgent = newSettings.defaultUserAgent ?? null;
}
return updated;
}; };
getDBSettings = async () => { getDBSettings = async () => {
+5 -1
View File
@@ -6,6 +6,10 @@ export type { GeoContinent } from "@/types/geoCheck.js";
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
export type MonitorType = (typeof MonitorTypes)[number]; export type MonitorType = (typeof MonitorTypes)[number];
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const; export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type); export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
@@ -49,8 +53,8 @@ export interface Monitor {
selectedDisks: string[]; selectedDisks: string[];
gameId?: string; gameId?: string;
grpcServiceName?: string; grpcServiceName?: string;
strategy?: PageSpeedStrategy;
group: string | null; group: string | null;
customUserAgent?: string | null;
geoCheckEnabled?: boolean; geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[]; geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number; geoCheckInterval?: number;
-1
View File
@@ -31,7 +31,6 @@ export interface Settings {
systemEmailRequireTLS: boolean; systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean; systemEmailRejectUnauthorized: boolean;
showURL: boolean; showURL: boolean;
defaultUserAgent?: string;
singleton: boolean; singleton: boolean;
version: number; version: number;
globalThresholds?: SettingsThresholds; globalThresholds?: SettingsThresholds;
+5 -5
View File
@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { booleanCoercion, httpHeaderValueSchema } from "./shared.js"; import { booleanCoercion } from "./shared.js";
import { GeoContinents } from "@/types/geoCheck.js"; import { GeoContinents } from "@/types/geoCheck.js";
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js"; import { MonitorMatchMethods, MonitorTypes, PageSpeedStrategies, DefaultPageSpeedStrategy } from "@/types/monitor.js";
export const getMonitorByIdParamValidation = z.object({ export const getMonitorByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"), monitorId: z.string().min(1, "Monitor ID is required"),
@@ -73,9 +73,9 @@ export const createMonitorBodyValidation = z.object({
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
gameId: z.union([z.string(), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""), grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
strategy: z.enum(PageSpeedStrategies).optional(),
selectedDisks: z.array(z.string()).optional(), selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
customUserAgent: httpHeaderValueSchema.nullable().optional(),
geoCheckEnabled: z.boolean().optional(), geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z.number().min(300000).optional(), geoCheckInterval: z.number().min(300000).optional(),
@@ -103,9 +103,9 @@ export const editMonitorBodyValidation = z.object({
tempAlertThreshold: z.number().optional(), tempAlertThreshold: z.number().optional(),
gameId: z.union([z.string(), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).optional(), grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
strategy: z.enum(PageSpeedStrategies).optional(),
selectedDisks: z.array(z.string()).optional(), selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
customUserAgent: httpHeaderValueSchema.nullable().optional(),
geoCheckEnabled: z.boolean().optional(), geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z.number().min(300000).optional(), geoCheckInterval: z.number().min(300000).optional(),
@@ -158,8 +158,8 @@ const importedMonitorSchema = z.object({
selectedDisks: z.array(z.string()).default([]), selectedDisks: z.array(z.string()).default([]),
gameId: z.union([z.string(), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""), grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
strategy: z.enum(PageSpeedStrategies).default(DefaultPageSpeedStrategy),
group: z.union([z.string().max(50).trim(), z.null()]).default(null), group: z.union([z.string().max(50).trim(), z.null()]).default(null),
customUserAgent: httpHeaderValueSchema.nullable().optional(),
geoCheckEnabled: z.boolean().default(false), geoCheckEnabled: z.boolean().default(false),
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]), geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
geoCheckInterval: z.number().min(300000).default(300000), geoCheckInterval: z.number().min(300000).default(300000),
@@ -1,6 +1,5 @@
import { CHECK_TTL_SENTINEL } from "@/types/check.js"; import { CHECK_TTL_SENTINEL } from "@/types/check.js";
import { z } from "zod"; import { z } from "zod";
import { httpHeaderValueSchema } from "@/validation/shared.js";
//**************************************** //****************************************
// Settings Validations // Settings Validations
@@ -21,7 +20,6 @@ export const updateAppSettingsBodyValidation = z
systemEmailTLSServername: z.string().nullable().optional(), systemEmailTLSServername: z.string().nullable().optional(),
showURL: z.boolean().optional(), showURL: z.boolean().optional(),
defaultUserAgent: httpHeaderValueSchema.nullable().optional(),
systemEmailSecure: z.boolean().optional(), systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(), systemEmailPool: z.boolean().optional(),
systemEmailIgnoreTLS: z.boolean().optional(), systemEmailIgnoreTLS: z.boolean().optional(),
-6
View File
@@ -21,12 +21,6 @@ export const booleanCoercion = z.preprocess((val) => {
return val; // Let Zod validation handle invalid values return val; // Let Zod validation handle invalid values
}, z.boolean()); }, 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[]) => { export const roleValidator = (allowedRoles: UserRole[]) => {
return z.array(z.custom<UserRole>()).refine((userRoles) => allowedRoles.some((role) => userRoles.includes(role)), { 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(", ")}`, message: `You do not have the required authorization. Required roles: ${allowedRoles.join(", ")}`,