From b7ba6330dbf0e69308c1c37bd1637b1863c46f17 Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Thu, 12 May 2022 18:18:47 +0800
Subject: [PATCH] Feat: Add cert exp. settings

---
 server/model/monitor.js                   | 19 ++++--
 src/components/ActionInput.vue            | 62 +++++++++++++++++++
 src/components/settings/Notifications.vue | 72 ++++++++++++++++++++++-
 src/languages/en.js                       |  2 +
 src/pages/Settings.vue                    |  4 ++
 5 files changed, 153 insertions(+), 6 deletions(-)
 create mode 100644 src/components/ActionInput.vue

diff --git a/server/model/monitor.js b/server/model/monitor.js
index eaafb775..e1dc00e6 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -7,7 +7,7 @@ dayjs.extend(timezone);
 const axios = require("axios");
 const { Prometheus } = require("../prometheus");
 const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
-const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server");
+const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync, setSetting } = require("../util-server");
 const { R } = require("redbean-node");
 const { BeanModel } = require("redbean-node/dist/bean-model");
 const { Notification } = require("../notification");
@@ -826,10 +826,19 @@ class Monitor extends BeanModel {
         if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
             const notificationList = await Monitor.getNotificationList(this);
 
-            log.debug("monitor", "call sendCertNotificationByTargetDays");
-            await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 21, notificationList);
-            await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 14, notificationList);
-            await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, 7, notificationList);
+            let notifyDays = await setting("tlsExpiryNotifyDays");
+            if (notifyDays == null || !Array.isArray(notifyDays)) {
+                // Reset Default
+                setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
+                notifyDays = [ 7, 14, 21 ];
+            }
+
+            if (notifyDays != null && Array.isArray(notifyDays)) {
+                for (const day of notifyDays) {
+                    log.debug("monitor", "call sendCertNotificationByTargetDays", day);
+                    await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
+                }
+            }
         }
     }
 
diff --git a/src/components/ActionInput.vue b/src/components/ActionInput.vue
new file mode 100644
index 00000000..00cb3aab
--- /dev/null
+++ b/src/components/ActionInput.vue
@@ -0,0 +1,62 @@
+<template>
+    <div class="input-group mb-3">
+        <input
+            ref="input"
+            v-model="model"
+            class="form-control"
+            :type="type"
+            :placeholder="placeholder"
+            :disabled="!enabled"
+        >
+        <a class="btn btn-outline-primary" @click="action()">
+            <font-awesome-icon :icon="icon" />
+        </a>
+    </div>
+</template>
+
+<script>
+export default {
+    props: {
+        modelValue: {
+            type: String,
+            default: ""
+        },
+        enabled: {
+            type: Boolean,
+            default: true
+        },
+        placeholder: {
+            type: String,
+            default: ""
+        },
+        icon: {
+            type: String,
+            required: true,
+        },
+        type: {
+            type: String,
+            default: "text",
+        },
+        action: {
+            type: Function,
+            default: () => {},
+        }
+    },
+    emits: [ "update:modelValue" ],
+    computed: {
+        model: {
+            get() {
+                return this.modelValue;
+            },
+            set(value) {
+                this.$emit("update:modelValue", value);
+            }
+        }
+    },
+    created() {
+
+    },
+    methods: {
+    }
+};
+</script>
diff --git a/src/components/settings/Notifications.vue b/src/components/settings/Notifications.vue
index b2cbcf48..187f4b7d 100644
--- a/src/components/settings/Notifications.vue
+++ b/src/components/settings/Notifications.vue
@@ -20,16 +20,74 @@
             </button>
         </div>
 
+        <div class="my-4">
+            <h4>{{ $t("settingsCertificateExpiry") }}</h4>
+            <p>{{ $t("certificationExpiryDescription") }}</p>
+            <div class="mt-2 mb-4 ps-2 cert-exp-days col-12 col-xl-6">
+                <div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
+                    <span>{{ day }} {{ $t("day(s)") }}</span>
+                    <button type="button" class="btn btn-outline-primary ms-2 px-3 py-1" @click="removeExpiryNotifDay(day)">
+                        <font-awesome-icon class="" icon="times" />
+                    </button>
+                </div>
+            </div>
+            <div class="col-12 col-xl-6">
+                <ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day(s)')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
+            </div>
+            <div>
+                <button class="btn btn-primary" type="button" @click="saveSettings()">
+                    {{ $t("Save") }}
+                </button>
+            </div>
+        </div>
+
         <NotificationDialog ref="notificationDialog" />
     </div>
 </template>
 
 <script>
 import NotificationDialog from "../../components/NotificationDialog.vue";
+import ActionInput from "../ActionInput.vue";
 
 export default {
     components: {
-        NotificationDialog
+        NotificationDialog,
+        ActionInput,
+    },
+
+    data() {
+        return {
+            expiryNotifInput: null,
+        };
+    },
+
+    computed: {
+        settings() {
+            return this.$parent.$parent.$parent.settings;
+        },
+        saveSettings() {
+            return this.$parent.$parent.$parent.saveSettings;
+        },
+        settingsLoaded() {
+            return this.$parent.$parent.$parent.settingsLoaded;
+        },
+    },
+
+    methods: {
+        removeExpiryNotifDay(day) {
+            this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
+        },
+        addExpiryNotifDay(day) {
+            if (day != null && day !== "") {
+                const parsedDay = parseInt(day);
+                if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
+                    if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) {
+                        this.settings.tlsExpiryNotifyDays.push(parseInt(day));
+                        this.expiryNotifInput = null;
+                    }
+                }
+            }
+        },
     },
 };
 </script>
@@ -43,4 +101,16 @@ export default {
         color: $dark-font-color;
     }
 }
+
+.cert-exp-days .cert-exp-day-row {
+    border-bottom: 1px solid rgba(0, 0, 0, 0.125);
+
+    .dark & {
+        border-bottom: 1px solid $dark-border-color;
+    }
+}
+
+.cert-exp-days .cert-exp-day-row:last-child {
+    border: none;
+}
 </style>
diff --git a/src/languages/en.js b/src/languages/en.js
index af6ec32b..d21e40ef 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -515,4 +515,6 @@ export default {
     "Go back to the previous page.": "Go back to the previous page.",
     "Coming Soon": "Coming Soon",
     wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
+    settingsCertificateExpiry: "TLS Certificate Expiry",
+    certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
 };
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue
index 11d3a1be..cb5f027c 100644
--- a/src/pages/Settings.vue
+++ b/src/pages/Settings.vue
@@ -145,6 +145,10 @@ export default {
                     this.settings.keepDataPeriodDays = 180;
                 }
 
+                if (this.settings.tlsExpiryNotifyDays === undefined) {
+                    this.settings.tlsExpiryNotifyDays = [];
+                }
+
                 this.settingsLoaded = true;
             });
         },