Merge branch 'develop' into feat/fallback-refactor
This commit is contained in:
commit
cb02bb0de7
12 files changed
+228
-147
No files matched your search
@@ -20,11 +20,3 @@ spec:
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets
|
||||
volumeMounts:
|
||||
- name: docker-sock
|
||||
mountPath: /var/run/docker.sock
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
hostPath:
|
||||
path: /var/run/docker.sock
|
||||
type: Socket
|
||||
@@ -5,6 +5,11 @@ metadata:
|
||||
name: server-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}"
|
||||
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
|
||||
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.server.ingress.host }}
|
||||
|
||||
@@ -977,7 +977,7 @@ class NetworkService {
|
||||
});
|
||||
}
|
||||
|
||||
return this.axiosInstance.get(`/monitors/summary/team?${params.toString()}`, {
|
||||
return this.axiosInstance.get(`/monitors/team/summary?${params.toString()}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -128,48 +128,37 @@ const monitorValidation = joi.object({
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
// Regex from https://gist.github.com/dperini/729294
|
||||
var urlRegex = new RegExp(
|
||||
"^" +
|
||||
// protocol identifier (optional)
|
||||
// short syntax // still required
|
||||
"(?:(?:https?|ftp):\\/\\/)?" +
|
||||
// user:pass BasicAuth (optional)
|
||||
"(?:" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broadcast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host & domain names, may end with dot
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
")?" +
|
||||
"[a-z0-9\\u00a1-\\uffff]\\." +
|
||||
")+" +
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$",
|
||||
"i"
|
||||
);
|
||||
if (!urlRegex.test(value)) {
|
||||
// 1. Standard URLs: must have protocol and pass canParse()
|
||||
if (/^(https?:\/\/)/.test(value)) {
|
||||
if (
|
||||
typeof URL !== "undefined" &&
|
||||
typeof URL.canParse === "function" &&
|
||||
URL.canParse(value)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
// else, it's a malformed URL with protocol
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
|
||||
return value;
|
||||
// 2. Docker/internal hostnames (no protocol)
|
||||
if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(value)) {
|
||||
if (value.includes("..")) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
// Split by dot, check each label
|
||||
const labels = value.split(":")[0].split(".");
|
||||
for (const label of labels) {
|
||||
if (!label) return helpers.error("string.invalidUrl");
|
||||
if (label.startsWith("-") || label.endsWith("-")) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 3. Everything else is invalid
|
||||
return helpers.error("string.invalidUrl");
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
|
||||
@@ -340,7 +340,6 @@ class AuthController {
|
||||
if (!email) {
|
||||
throw new Error("No email in request");
|
||||
}
|
||||
// 1. Find all the monitors associated with the team ID if superadmin
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
const userId = req?.user?._id;
|
||||
@@ -354,7 +353,11 @@ class AuthController {
|
||||
}
|
||||
|
||||
const roles = req.user.role;
|
||||
if (roles.includes("demo")) {
|
||||
throw new Error("Demo user cannot be deleted");
|
||||
}
|
||||
|
||||
// 1. Find all the monitors associated with the team ID if superadmin
|
||||
const result = await this.db.getMonitorsByTeamId({
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import axios from "axios";
|
||||
import seedDb from "../db/mongo/utils/seedDb.js";
|
||||
import pkg from "papaparse";
|
||||
import { asyncHandler, createServerError } from "../utils/errorUtils.js";
|
||||
import { fetchMonitorCertificate } from "./controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
@@ -31,6 +32,15 @@ class MonitorController {
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
async verifyTeamAccess(teamId, monitorId) {
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const error = new Error("Unauthorized");
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all monitors
|
||||
* @async
|
||||
@@ -52,32 +62,18 @@ class MonitorController {
|
||||
"getAllMonitors"
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns all monitors with uptime stats for 1,7,30, and 90 days
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @param {function} next
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
getAllMonitorsWithUptimeStats = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const monitors = await this.db.getAllMonitorsWithUptimeStats();
|
||||
return res.success({
|
||||
msg: this.stringService.monitorGetAll,
|
||||
data: monitors,
|
||||
});
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAllMonitorsWithUptimeStats"
|
||||
);
|
||||
|
||||
getUptimeDetailsById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { monitorId } = req.params;
|
||||
const { dateRange, normalize } = req.query;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("Team ID is required");
|
||||
}
|
||||
|
||||
await this.verifyTeamAccess(teamId, monitorId);
|
||||
|
||||
const data = await this.db.getUptimeDetailsById({
|
||||
monitorId,
|
||||
dateRange,
|
||||
@@ -109,6 +105,13 @@ class MonitorController {
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const { monitorId } = req.params;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("Team ID is required");
|
||||
}
|
||||
|
||||
await this.verifyTeamAccess(teamId, monitorId);
|
||||
|
||||
const monitorStats = await this.db.getMonitorStatsById({
|
||||
monitorId,
|
||||
limit,
|
||||
@@ -140,8 +143,14 @@ class MonitorController {
|
||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const { monitorId } = req.params;
|
||||
const { dateRange } = req.query;
|
||||
const monitorId = req?.params?.monitorId;
|
||||
const dateRange = req?.query?.dateRange;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("Team ID is required");
|
||||
}
|
||||
|
||||
await this.verifyTeamAccess(teamId, monitorId);
|
||||
const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
|
||||
return res.success({
|
||||
msg: this.stringService.monitorGetByIdSuccess,
|
||||
@@ -153,7 +162,7 @@ class MonitorController {
|
||||
);
|
||||
|
||||
getMonitorCertificate = asyncHandler(
|
||||
async (req, res, next, fetchMonitorCertificate) => {
|
||||
async (req, res, next) => {
|
||||
await getCertificateParamValidation.validateAsync(req.params);
|
||||
|
||||
const { monitorId } = req.params;
|
||||
@@ -188,6 +197,18 @@ class MonitorController {
|
||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const monitor = await this.db.getMonitorById(req.params.monitorId);
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("Team ID is required");
|
||||
}
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const error = new Error("Unauthorized");
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.monitorGetByIdSuccess,
|
||||
data: monitor,
|
||||
@@ -367,6 +388,13 @@ class MonitorController {
|
||||
async (req, res, next) => {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
const monitorId = req.params.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("Team ID is required");
|
||||
}
|
||||
|
||||
await this.verifyTeamAccess(teamId, monitorId);
|
||||
|
||||
const monitor = await this.db.deleteMonitor({ monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.deleteStatusPagesByMonitorId(monitor._id);
|
||||
@@ -389,7 +417,7 @@ class MonitorController {
|
||||
*/
|
||||
deleteAllMonitors = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { teamId } = req.user;
|
||||
const teamId = req?.user?.teamId;
|
||||
const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId);
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
@@ -431,7 +459,14 @@ class MonitorController {
|
||||
async (req, res, next) => {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await editMonitorBodyValidation.validateAsync(req.body);
|
||||
const { monitorId } = req.params;
|
||||
const monitorId = req?.params?.monitorId;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("Team ID is required");
|
||||
}
|
||||
|
||||
await this.verifyTeamAccess(teamId, monitorId);
|
||||
|
||||
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
|
||||
|
||||
@@ -462,6 +497,13 @@ class MonitorController {
|
||||
await pauseMonitorParamValidation.validateAsync(req.params);
|
||||
|
||||
const monitorId = req.params.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("Team ID is required");
|
||||
}
|
||||
|
||||
await this.verifyTeamAccess(teamId, monitorId);
|
||||
|
||||
const monitor = await this.db.pauseMonitor({ monitorId });
|
||||
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
|
||||
|
||||
|
||||
@@ -39,9 +39,19 @@ class NotificationController {
|
||||
});
|
||||
|
||||
const body = req.body;
|
||||
const { _id, teamId } = req.user;
|
||||
body.userId = _id;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const userId = req?.user?._id;
|
||||
if (!userId) {
|
||||
throw new Error("No user ID in request");
|
||||
}
|
||||
body.userId = userId;
|
||||
body.teamId = teamId;
|
||||
|
||||
const notification = await this.db.createNotification(body);
|
||||
return res.success({
|
||||
msg: "Notification created successfully",
|
||||
@@ -54,7 +64,13 @@ class NotificationController {
|
||||
|
||||
getNotificationsByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const notifications = await this.db.getNotificationsByTeamId(req.user.teamId);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const notifications = await this.db.getNotificationsByTeamId(teamId);
|
||||
|
||||
return res.success({
|
||||
msg: "Notifications fetched successfully",
|
||||
data: notifications,
|
||||
@@ -66,6 +82,18 @@ class NotificationController {
|
||||
|
||||
deleteNotification = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const notification = await this.db.getNotificationById(req.params.id);
|
||||
if (!notification.teamId.equals(teamId)) {
|
||||
const error = new Error("Unauthorized");
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.db.deleteNotificationById(req.params.id);
|
||||
return res.success({
|
||||
msg: "Notification deleted successfully",
|
||||
@@ -78,6 +106,17 @@ class NotificationController {
|
||||
getNotificationById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const notification = await this.db.getNotificationById(req.params.id);
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
if (!notification.teamId.equals(teamId)) {
|
||||
const error = new Error("Unauthorized");
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
return res.success({
|
||||
msg: "Notification fetched successfully",
|
||||
data: notification,
|
||||
@@ -93,10 +132,23 @@ class NotificationController {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
const notification = await this.db.editNotification(req.params.id, req.body);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const notification = await this.db.getNotificationById(req.params.id);
|
||||
|
||||
if (!notification.teamId.equals(teamId)) {
|
||||
const error = new Error("Unauthorized");
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const editedNotification = await this.db.editNotification(req.params.id, req.body);
|
||||
return res.success({
|
||||
msg: "Notification updated successfully",
|
||||
data: notification,
|
||||
data: editedNotification,
|
||||
});
|
||||
},
|
||||
SERVICE_NAME,
|
||||
@@ -105,8 +157,20 @@ class NotificationController {
|
||||
|
||||
testAllNotifications = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { monitorId } = req.body;
|
||||
const monitorId = req.body.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const error = new Error("Unauthorized");
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const notifications = monitor.notifications;
|
||||
if (notifications.length === 0) throw new Error("No notifications");
|
||||
const result = await this.notificationService.testAllNotifications(notifications);
|
||||
|
||||
@@ -56,53 +56,6 @@ const getAllMonitors = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all monitors with uptime stats for 1,7,30, and 90 days
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Array<Monitor>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getAllMonitorsWithUptimeStats = async () => {
|
||||
const timeRanges = {
|
||||
1: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
7: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
30: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
90: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
|
||||
};
|
||||
|
||||
try {
|
||||
const monitors = await Monitor.find();
|
||||
const monitorsWithStats = await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
const model = CHECK_MODEL_LOOKUP[monitor.type];
|
||||
|
||||
const uptimeStats = await Promise.all(
|
||||
Object.entries(timeRanges).map(async ([days, startDate]) => {
|
||||
const checks = await model.find({
|
||||
monitorId: monitor._id,
|
||||
createdAt: { $gte: startDate },
|
||||
});
|
||||
return [days, getUptimePercentage(checks)];
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...monitor.toObject(),
|
||||
...Object.fromEntries(uptimeStats),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return monitorsWithStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getAllMonitorsWithUptimeStats";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to calculate uptime duration based on the most recent check.
|
||||
* @param {Array} checks Array of check objects.
|
||||
@@ -761,7 +714,6 @@ const pauseMonitor = async ({ monitorId }) => {
|
||||
|
||||
export {
|
||||
getAllMonitors,
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsByIds,
|
||||
|
||||
@@ -74,6 +74,11 @@ const deleteNotificationsByMonitorId = async (monitorId) => {
|
||||
|
||||
const deleteNotificationById = async (id) => {
|
||||
try {
|
||||
const notification = await Notification.findById(id);
|
||||
if (!notification) {
|
||||
throw new Error("Notification not found");
|
||||
}
|
||||
|
||||
const result = await Notification.findByIdAndDelete(id);
|
||||
await Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } });
|
||||
return result;
|
||||
|
||||
@@ -20,10 +20,9 @@ class MonitorRoutes {
|
||||
// Team routes
|
||||
this.router.get("/team", this.monitorController.getMonitorsByTeamId);
|
||||
this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId);
|
||||
this.router.get("/summary/team", this.monitorController.getMonitorsAndSummaryByTeamId); // TODO should be /team/summary
|
||||
this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId);
|
||||
|
||||
// Uptime routes
|
||||
this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats);
|
||||
this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById);
|
||||
|
||||
// Hardware routes
|
||||
@@ -53,13 +52,8 @@ class MonitorRoutes {
|
||||
|
||||
// Individual monitor CRUD routes
|
||||
this.router.get("/:monitorId", this.monitorController.getMonitorById);
|
||||
this.router.put("/:monitorId", verifyTeamAccess(Monitor, "monitorId"), isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor);
|
||||
this.router.delete(
|
||||
"/:monitorId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
this.monitorController.deleteMonitor
|
||||
);
|
||||
this.router.put("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor);
|
||||
this.router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.deleteMonitor);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Router } from "express";
|
||||
import { verifyJWT } from "../middleware/verifyJWT.js";
|
||||
import { verifyOwnership } from "../middleware/verifyOwnership.js";
|
||||
import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
|
||||
import Notification from "../db/models/Notification.js";
|
||||
class NotificationRoutes {
|
||||
constructor(notificationController) {
|
||||
this.router = Router();
|
||||
@@ -11,8 +7,6 @@ class NotificationRoutes {
|
||||
}
|
||||
|
||||
initializeRoutes() {
|
||||
this.router.use(verifyJWT);
|
||||
|
||||
this.router.post("/", this.notificationController.createNotification);
|
||||
|
||||
this.router.post("/test/all", this.notificationController.testAllNotifications);
|
||||
@@ -20,9 +14,9 @@ class NotificationRoutes {
|
||||
|
||||
this.router.get("/team", this.notificationController.getNotificationsByTeamId);
|
||||
|
||||
this.router.delete("/:id", verifyOwnership(Notification, "id"), this.notificationController.deleteNotification);
|
||||
this.router.get("/:id", this.notificationController.getNotificationById);
|
||||
this.router.put("/:id", verifyTeamAccess(Notification, "id"), this.notificationController.editNotification);
|
||||
this.router.delete("/:id", this.notificationController.deleteNotification);
|
||||
this.router.put("/:id", this.notificationController.editNotification);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -158,7 +158,48 @@ const createMonitorBodyValidation = joi.object({
|
||||
name: joi.string().required(),
|
||||
description: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
url: joi.string().required(),
|
||||
url: joi
|
||||
.string()
|
||||
.required()
|
||||
.custom((value, helpers) => {
|
||||
// 1. Standard URLs: must have protocol and pass canParse()
|
||||
if (/^(https?:\/\/)/.test(value)) {
|
||||
if (
|
||||
typeof URL !== "undefined" &&
|
||||
typeof URL.canParse === "function" &&
|
||||
URL.canParse(value)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
// else, it's a malformed URL with protocol
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
|
||||
// 2. Docker/internal hostnames (no protocol)
|
||||
if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(value)) {
|
||||
if (value.includes("..")) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
// Split by dot, check each label
|
||||
const labels = value.split(":")[0].split(".");
|
||||
for (const label of labels) {
|
||||
if (!label) return helpers.error("string.invalidUrl");
|
||||
if (label.startsWith("-") || label.endsWith("-")) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 3. Everything else is invalid
|
||||
return helpers.error("string.invalidUrl");
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
"string.invalidUrl":
|
||||
"Please enter a valid URL, hostname, or container name (with optional port).",
|
||||
}),
|
||||
ignoreTlsErrors: joi.boolean().default(false),
|
||||
port: joi.number(),
|
||||
isActive: joi.boolean(),
|
||||
|
||||
Reference in new issue
Block a user