From ce740724d85bf083a10353c94a06ff87916093be Mon Sep 17 00:00:00 2001 From: Rohit Darekar Date: Sun, 1 Mar 2026 05:08:29 +0530 Subject: [PATCH] fix: preserve Domain Name Expiry Notification setting when editing monitor (#6994) Co-authored-by: Louis Lam --- server/model/domain_expiry.js | 12 +-- server/server.js | 3 +- src/lang/en.json | 4 +- src/pages/EditMonitor.vue | 89 +++++++++++-------- test/backend-test/test-domain.js | 32 ------- .../specs/domain-expiry-notification.spec.js | 84 +---------------- 6 files changed, 61 insertions(+), 163 deletions(-) diff --git a/server/model/domain_expiry.js b/server/model/domain_expiry.js index fafcffa2c..3e11599a2 100644 --- a/server/model/domain_expiry.js +++ b/server/model/domain_expiry.js @@ -220,17 +220,11 @@ class DomainExpiry extends BeanModel { const tld = parseTld(target); - // Avoid logging for incomplete/invalid input while editing monitors. - if (tld.isIp) { - throw new TranslatableError("domain_expiry_unsupported_is_ip", { hostname: tld.hostname }); - } - // No one-letter public suffix exists; treat this as an incomplete/invalid input while typing. - if (tld.publicSuffix.length < 2) { - throw new TranslatableError("domain_expiry_public_suffix_too_short", { publicSuffix: tld.publicSuffix }); - } + // It must be checked first, filter out non-ICANN domains. if (!tld.isIcann) { throw new TranslatableError("domain_expiry_unsupported_is_icann", { - domain: tld.domain, + // If domain is null, use hostname as fallback for better error message. + domain: tld.domain ?? tld.hostname ?? "EMPTY DOMAIN", publicSuffix: tld.publicSuffix, }); } diff --git a/server/server.js b/server/server.js index 49d995e9b..6d2aff970 100644 --- a/server/server.js +++ b/server/server.js @@ -1003,7 +1003,8 @@ let needSetup = false; } }); - socket.on("checkMointor", async (partial, callback) => { + // partial { type, url, hostname, grpcUrl } + socket.on("checkDomain", async (partial, callback) => { try { checkLogin(socket); const DomainExpiry = require("./model/domain_expiry"); diff --git a/src/lang/en.json b/src/lang/en.json index 481299fba..3e6b2638b 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1407,9 +1407,7 @@ "domainExpiryDescription": "Trigger notification when domain names expires in:", "domain_expiry_unsupported_monitor_type": "Domain expiry monitoring is not supported for this monitor type", "domain_expiry_unsupported_missing_target": "No valid domain or hostname is configured for this monitor", - "domain_expiry_public_suffix_too_short": "\".{publicSuffix}\" is too short for a top level domain", - "domain_expiry_unsupported_is_icann": "The domain \"{domain}\" is not a candidate for domain expiry monitoring, because its public suffix \".{publicSuffix}\" is not ICAN", - "domain_expiry_unsupported_is_ip": "\"{hostname}\" is an IP address. Domain expiry monitoring requires a domain name", + "domain_expiry_unsupported_is_icann": "The domain \"{domain}\" is not a candidate for domain expiry monitoring, because its public suffix \".{publicSuffix}\" is not managed by ICANN", "domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint": "Domain expiry monitoring is not available for \".{publicSuffix}\" because no RDAP service is listed by IANA", "minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.", "lowIntervalWarning": "Are you sure want to set the interval value below 20 seconds? Performance may be degraded, particularly if there are a large number of monitors.", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 359d75cf1..e7387bd4b 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -1548,15 +1548,17 @@ v-model="monitor.domainExpiryNotification" class="form-check-input" type="checkbox" - :disabled="!hasDomain" /> -
+
{{ $t("domainExpiryNotificationHelp") }}
-
+
{{ domainExpiryUnsupportedReason }}
@@ -2867,7 +2869,7 @@ const monitorDefaults = { ignoreTls: false, upsideDown: false, expiryNotification: false, - domainExpiryNotification: false, + domainExpiryNotification: true, maxredirects: 10, accepted_statuscodes: defaultValueList.http.accepted_statuscodes, saveResponse: false, @@ -2929,9 +2931,8 @@ export default { notificationIDList: {}, // Do not add default value here, please check init() method }, - hasDomain: false, domainExpiryUnsupportedReason: null, - checkMonitorDebounce: null, + checkDomainDebounce: null, acceptedStatusCodeOptions: [], acceptedWebsocketCodeOptions: [], dnsresolvetypeOptions: [], @@ -2990,16 +2991,6 @@ export default { return this.$t("defaultFriendlyName"); }, - monitorTypeUrlHost() { - const { type, url, hostname, grpcUrl } = this.monitor; - return { - type, - url, - hostname, - grpcUrl, - }; - }, - showDomainExpiryNotification() { return this.monitor.type in TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD; }, @@ -3293,30 +3284,25 @@ message HealthCheckResponse { } }, - monitorTypeUrlHost(data) { - if (this.checkMonitorDebounce != null) { - clearTimeout(this.checkMonitorDebounce); - } + showDomainExpiryNotification() { + this.checkDomain(); + }, - if (!this.showDomainExpiryNotification) { - this.hasDomain = false; - this.domainExpiryUnsupportedReason = null; - return; - } + "monitor.hostname"() { + this.checkDomain(); + }, - this.checkMonitorDebounce = setTimeout(() => { - this.$root.getSocket().emit("checkMointor", data, (res) => { - const wasSupported = this.hasDomain; - this.hasDomain = !!res?.ok; - if (this.hasDomain !== wasSupported) { - this.monitor.domainExpiryNotification = this.hasDomain; - } - this.domainExpiryUnsupportedReason = res.msgi18n ? this.$t(res.msg, res.meta) : res.msg; - }); - }, 500); + "monitor.url"() { + this.checkDomain(); + }, + + "monitor.grpcUrl"() { + this.checkDomain(); }, "monitor.type"(newType, oldType) { + this.checkDomain(); + if (newType === "globalping" && !this.monitor.subtype) { this.monitor.subtype = "ping"; } @@ -4015,6 +4001,39 @@ message HealthCheckResponse { } } }, + + // Check Domain + // Do nothing if not checked + checkDomain() { + console.log("checkDomain called"); + if (this.checkDomainDebounce != null) { + clearTimeout(this.checkDomainDebounce); + } + + if (!this.showDomainExpiryNotification) { + this.domainExpiryUnsupportedReason = null; + return; + } + + this.checkDomainDebounce = setTimeout(() => { + const { type, url, hostname, grpcUrl } = this.monitor; + const data = { + type, + url, + hostname, + grpcUrl, + }; + + this.$root.getSocket().emit("checkDomain", data, (res) => { + console.log(data); + if (!res.ok) { + this.domainExpiryUnsupportedReason = res.msgi18n ? this.$t(res.msg, res.meta) : res.msg; + } else { + this.domainExpiryUnsupportedReason = null; + } + }); + }, 500); + }, }, }; diff --git a/test/backend-test/test-domain.js b/test/backend-test/test-domain.js index 80d448aed..267fec1e1 100644 --- a/test/backend-test/test-domain.js +++ b/test/backend-test/test-domain.js @@ -96,38 +96,6 @@ describe("Domain Expiry", () => { }); describe("Domain Parsing", () => { - test("throws error for IP address (isIp check)", async () => { - const monitor = { - type: "http", - url: "https://127.0.0.1", - domainExpiryNotification: true, - }; - await assert.rejects( - async () => await DomainExpiry.checkSupport(monitor), - (error) => { - assert.strictEqual(error.constructor.name, "TranslatableError"); - assert.strictEqual(error.message, "domain_expiry_unsupported_is_ip"); - return true; - } - ); - }); - - test("throws error for too short suffix(example.a)", async () => { - const monitor = { - type: "http", - url: "https://example.a", - domainExpiryNotification: true, - }; - await assert.rejects( - async () => await DomainExpiry.checkSupport(monitor), - (error) => { - assert.strictEqual(error.constructor.name, "TranslatableError"); - assert.strictEqual(error.message, "domain_expiry_public_suffix_too_short"); - return true; - } - ); - }); - test("throws error for non-ICANN TLD (e.g. .local)", async () => { const monitor = { type: "http", diff --git a/test/e2e/specs/domain-expiry-notification.spec.js b/test/e2e/specs/domain-expiry-notification.spec.js index 9ac15ec21..94c40832a 100644 --- a/test/e2e/specs/domain-expiry-notification.spec.js +++ b/test/e2e/specs/domain-expiry-notification.spec.js @@ -6,7 +6,7 @@ test.describe("Domain Expiry Notification", () => { await restoreSqliteSnapshot(page); }); - test("supported TLD auto-enables checkbox", async ({ page }, testInfo) => { + test("TLD enabled for new monitor", async ({ page }, testInfo) => { await page.goto("./add"); await login(page); @@ -17,88 +17,6 @@ test.describe("Domain Expiry Notification", () => { const checkbox = page.getByLabel("Domain Name Expiry Notification"); await expect(checkbox).toBeChecked(); - await expect(checkbox).toBeEnabled(); - - await screenshot(testInfo, page); - }); - - test("unsupported TLD leaves checkbox disabled", async ({ page }, testInfo) => { - await page.goto("./add"); - await login(page); - - const monitorTypeSelect = page.getByTestId("monitor-type-select"); - await monitorTypeSelect.selectOption("http"); - - await page.getByTestId("url-input").fill("https://example.co"); - - const checkbox = page.getByLabel("Domain Name Expiry Notification"); - await expect(checkbox).not.toBeChecked(); - await expect(checkbox).toBeDisabled(); - - await screenshot(testInfo, page); - }); - - test("switching from supported to unsupported TLD disables checkbox", async ({ page }, testInfo) => { - await page.goto("./add"); - await login(page); - - const monitorTypeSelect = page.getByTestId("monitor-type-select"); - await monitorTypeSelect.selectOption("http"); - - const urlInput = page.getByTestId("url-input"); - const checkbox = page.getByLabel("Domain Name Expiry Notification"); - - await urlInput.fill("https://example.com"); - await expect(checkbox).toBeChecked(); - - await urlInput.fill("https://example.co"); - await expect(checkbox).not.toBeChecked(); - await expect(checkbox).toBeDisabled(); - - await screenshot(testInfo, page); - }); - - test("switching from unsupported to supported TLD enables checkbox", async ({ page }, testInfo) => { - await page.goto("./add"); - await login(page); - - const monitorTypeSelect = page.getByTestId("monitor-type-select"); - await monitorTypeSelect.selectOption("http"); - - const urlInput = page.getByTestId("url-input"); - const checkbox = page.getByLabel("Domain Name Expiry Notification"); - - await urlInput.fill("https://example.co"); - await expect(checkbox).not.toBeChecked(); - - await urlInput.fill("https://example.com"); - await expect(checkbox).toBeChecked(); - await expect(checkbox).toBeEnabled(); - - await screenshot(testInfo, page); - }); - - test("manual uncheck preserved when URL changes within same TLD", async ({ page }, testInfo) => { - await page.goto("./add"); - await login(page); - - const monitorTypeSelect = page.getByTestId("monitor-type-select"); - await monitorTypeSelect.selectOption("http"); - - const urlInput = page.getByTestId("url-input"); - const checkbox = page.getByLabel("Domain Name Expiry Notification"); - - await urlInput.fill("https://example.com"); - await expect(checkbox).toBeChecked(); - - await checkbox.uncheck(); - await expect(checkbox).not.toBeChecked(); - - await urlInput.fill("https://example.com/different-path"); - // Wait for debounce to fire and verify checkbox stays unchecked - await page.waitForTimeout(600); - await expect(checkbox).not.toBeChecked(); - await expect(checkbox).toBeEnabled(); await screenshot(testInfo, page); });