diff --git a/server/routers/api-router.js b/server/routers/api-router.js index 761b488e4..c33ef78c1 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -11,7 +11,17 @@ const { R } = require("redbean-node"); const apicache = require("../modules/apicache"); const Monitor = require("../model/monitor"); const dayjs = require("dayjs"); -const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util"); +const { + UP, + MAINTENANCE, + DOWN, + PENDING, + flipStatus, + log, + badgeConstants, + durationUnits, + isNumeric, +} = require("../../src/util"); const StatusPage = require("../model/status_page"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const { makeBadge } = require("badge-maker"); @@ -232,6 +242,11 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24"; const overrideValue = value && parseFloat(value); + if (isNumeric(requestedDuration)) { // all numeric only + requestedDuration = `${requestedDuration}${durationUnits.HOUR}`; + } + const duration = requestedDuration.slice(0, -1); + let publicMonitor = await R.getRow(` SELECT monitor_group.monitor_id FROM monitor_group, \`group\` WHERE monitor_group.group_id = \`group\`.id @@ -249,7 +264,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques badgeValues.color = badgeConstants.naColor; } else { const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId); - const uptime = overrideValue ?? uptimeCalculator.getDataByDuration(`${requestedDuration}h`).uptime; + const uptime = overrideValue ?? uptimeCalculator.getDataByDuration(requestedDuration).uptime; // limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits const cleanUptime = (uptime * 100).toPrecision(4); @@ -261,7 +276,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques // build a label string. If a custom label is given, override the default one (requestedDuration) badgeValues.label = filterAndJoin([ labelPrefix, - label ?? `Uptime (${requestedDuration}${labelSuffix})`, + label ?? `Uptime (${duration}${labelSuffix})`, ]); badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]); } @@ -298,13 +313,17 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24"; const overrideValue = value && parseFloat(value); + if (isNumeric(requestedDuration)) { // all numeric only + requestedDuration = `${requestedDuration}${durationUnits.HOUR}`; + } + const duration = requestedDuration.slice(0, -1); + // Check if monitor is public const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId); - const publicAvgPing = uptimeCalculator.getDataByDuration(`${requestedDuration}h`).avgPing; + const publicAvgPing = uptimeCalculator.getDataByDuration(requestedDuration).avgPing; const badgeValues = { style }; - if (!publicAvgPing) { // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant @@ -317,7 +336,7 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, // use a given, custom labelColor or use the default badge label color (defined by badge-maker) badgeValues.labelColor = labelColor ?? ""; // build a lable string. If a custom label is given, override the default one (requestedDuration) - badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration}${labelSuffix})` ]); + badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${duration}${labelSuffix})` ]); badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]); } diff --git a/server/uptime-calculator.js b/server/uptime-calculator.js index 37dfd6771..df9c0d14f 100644 --- a/server/uptime-calculator.js +++ b/server/uptime-calculator.js @@ -1,5 +1,12 @@ const dayjs = require("dayjs"); -const { UP, MAINTENANCE, DOWN, PENDING } = require("../src/util"); +const { + UP, + MAINTENANCE, + DOWN, + PENDING, + durationUnits, + isNumeric +} = require("../src/util"); const { LimitQueue } = require("./utils/limit-queue"); const { log } = require("../src/util"); const { R } = require("redbean-node"); @@ -543,7 +550,9 @@ class UptimeCalculator { if (type === "minute" && num > 24 * 60) { throw new Error("The maximum number of minutes is 1440"); } - + if (type === "day" && num > 365) { + throw new Error("The maximum number of days is 365"); + } // Get the current time period key based on the type let key = this.getKey(this.getCurrentDate(), type); @@ -742,24 +751,36 @@ class UptimeCalculator { /** * Get the uptime data for given duration. - * @param {string} duration A string with a number and a unit (h, d, or y), such as 24h, 30d, 1y. + * @param {string} duration A string with a number and a unit (m,h,d,w,M,y), such as 24h, 30d, 1y. * @returns {UptimeDataResult} UptimeDataResult - * @throws {Error} Invalid duration + * @throws {Error} Invalid duration / Unsupported unit */ getDataByDuration(duration) { - const unit = duration.substring(duration.length - 1); - const num = parseInt(duration.substring(0, duration.length - 1)); + const durationNumStr = duration.slice(0, -1); - const typeMap = { - h: "hour", - d: "day", - y: "year" - }; - - if (!Object.keys(typeMap).includes(unit)) { - throw new Error("Invalid duration"); + if (!isNumeric(durationNumStr)) { + throw new Error(`Invalid duration: ${duration}`); + } + const num = Number(durationNumStr); + const unit = duration.slice(-1); + + switch (unit) { + case durationUnits.MINUTE: + return this.getData(num, "minute"); + case durationUnits.HOUR: + return this.getData(num, "hour"); + case durationUnits.DAY: + return this.getData(num, "day"); + case durationUnits.WEEK: + return this.getData(7 * num, "day"); + case durationUnits.MONTH: + return this.getData(30 * num, "day"); + case durationUnits.YEAR: + return this.getData(365 * num, "day"); + default: + throw new Error(`Unsupported unit (${unit}) for badge duration ${duration}` + ); } - return this.getData(num, typeMap[unit]); } /** diff --git a/src/util.js b/src/util.js index b3706ddbf..63f403dd0 100644 --- a/src/util.js +++ b/src/util.js @@ -10,8 +10,8 @@ */ var _a; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0; -exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0; +exports.flipStatus = exports.durationUnits = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0; +exports.isNumeric = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = void 0; const dayjs = require("dayjs"); exports.isDev = process.env.NODE_ENV === "development"; exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node); @@ -96,6 +96,14 @@ exports.badgeConstants = { defaultCertExpireWarnDays: "14", defaultCertExpireDownDays: "7" }; +exports.durationUnits = { + MINUTE: "m", + HOUR: "h", + DAY: "d", + WEEK: "w", + MONTH: "M", + YEAR: "y" +}; function flipStatus(s) { if (s === exports.UP) { return exports.DOWN; @@ -396,3 +404,7 @@ function intHash(str, length = 10) { return (hash % length + length) % length; } exports.intHash = intHash; +function isNumeric(str) { + return /^([0-9]+)$/.test(str); +} +exports.isNumeric = isNumeric; diff --git a/src/util.ts b/src/util.ts index b4a52f508..75d01c789 100644 --- a/src/util.ts +++ b/src/util.ts @@ -93,11 +93,6 @@ const consoleLevelColors : Record = { "DEBUG": CONSOLE_STYLE_FgGray, }; -/** - * Flip the status of s - * @param s input status: UP or DOWN - * @returns {number} UP or DOWN - */ export const badgeConstants = { naColor: "#999", defaultUpColor: "#66c20a", @@ -118,7 +113,20 @@ export const badgeConstants = { defaultCertExpireDownDays: "7" }; -/** Flip the status of s */ +export const durationUnits = { + MINUTE: "m", + HOUR: "h", + DAY: "d", + WEEK: "w", + MONTH: "M", + YEAR: "y" +}; + +/** + * Flip the status of s + * @param s input status: UP or DOWN + * @returns {number} UP or DOWN + */ export function flipStatus(s: number) { if (s === UP) { return DOWN; @@ -643,3 +651,11 @@ export function intHash(str : string, length = 10) : number { return (hash % length + length) % length; // Ensure the result is non-negative } +/** + * Check if a string is numeric. + * @param {string} str - The input string to check + * @returns {boolean} Returns true if the input string is numeric, false otherwise + */ +export function isNumeric(str: string): boolean { + return /^([0-9]+)$/.test(str); +}