diff options
author | Daniel Baumann <daniel@debian.org> | 2024-11-26 09:28:28 +0100 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-11-26 12:25:58 +0100 |
commit | a1882b67c41fe9901a0cd8059b5cc78a5beadec0 (patch) | |
tree | 2a24507c67aa99a15416707b2f7e645142230ed8 /src/mixins/socket.js | |
parent | Initial commit. (diff) | |
download | uptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.tar.xz uptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.zip |
Adding upstream version 2.0.0~beta.0+dfsg.upstream/2.0.0_beta.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'src/mixins/socket.js')
-rw-r--r-- | src/mixins/socket.js | 879 |
1 files changed, 879 insertions, 0 deletions
diff --git a/src/mixins/socket.js b/src/mixins/socket.js new file mode 100644 index 0000000..3272e04 --- /dev/null +++ b/src/mixins/socket.js @@ -0,0 +1,879 @@ +import { io } from "socket.io-client"; +import { useToast } from "vue-toastification"; +import jwtDecode from "jwt-decode"; +import Favico from "favico.js"; +import dayjs from "dayjs"; +import mitt from "mitt"; + +import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts"; +import { getDevContainerServerHostname, isDevContainer, getToastSuccessTimeout, getToastErrorTimeout } from "../util-frontend.js"; +const toast = useToast(); + +let socket; + +const noSocketIOPages = [ + /^\/status-page$/, // /status-page + /^\/status/, // /status** + /^\/$/ // / +]; + +const favicon = new Favico({ + animation: "none" +}); + +export default { + + data() { + return { + info: { }, + socket: { + token: null, + firstConnect: true, + connected: false, + connectCount: 0, + initedSocketIO: false, + }, + username: null, + remember: (localStorage.remember !== "0"), + allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed. + loggedIn: false, + monitorList: { }, + monitorTypeList: {}, + maintenanceList: {}, + apiKeyList: {}, + heartbeatList: { }, + avgPingList: { }, + uptimeList: { }, + tlsInfoList: {}, + notificationList: [], + dockerHostList: [], + remoteBrowserList: [], + statusPageListLoaded: false, + statusPageList: [], + proxyList: [], + connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`, + showReverseProxyGuide: true, + cloudflared: { + cloudflareTunnelToken: "", + installed: null, + running: false, + message: "", + errorMessage: "", + currentPassword: "", + }, + faviconUpdateDebounce: null, + emitter: mitt(), + }; + }, + + created() { + this.initSocketIO(); + }, + + methods: { + + /** + * Initialize connection to socket server + * @param {boolean} bypass Should the check for if we + * are on a status page be bypassed? + * @returns {void} + */ + initSocketIO(bypass = false) { + // No need to re-init + if (this.socket.initedSocketIO) { + return; + } + + // No need to connect to the socket.io for status page + if (! bypass && location.pathname) { + for (let page of noSocketIOPages) { + if (location.pathname.match(page)) { + return; + } + } + } + + // Also don't need to connect to the socket.io for setup database page + if (location.pathname === "/setup-database") { + return; + } + + this.socket.initedSocketIO = true; + + let protocol = location.protocol + "//"; + + let url; + const env = process.env.NODE_ENV || "production"; + if (env === "development" && isDevContainer()) { + url = protocol + getDevContainerServerHostname(); + } else if (env === "development" || localStorage.dev === "dev") { + url = protocol + location.hostname + ":3001"; + } else { + // Connect to the current url + url = undefined; + } + + socket = io(url); + + socket.on("info", (info) => { + this.info = info; + }); + + socket.on("setup", (monitorID, data) => { + this.$router.push("/setup"); + }); + + socket.on("autoLogin", (monitorID, data) => { + this.loggedIn = true; + this.storage().token = "autoLogin"; + this.socket.token = "autoLogin"; + this.allowLoginDialog = false; + }); + + socket.on("loginRequired", () => { + let token = this.storage().token; + if (token && token !== "autoLogin") { + this.loginByToken(token); + } else { + this.$root.storage().removeItem("token"); + this.allowLoginDialog = true; + } + }); + + socket.on("monitorList", (data) => { + this.assignMonitorUrlParser(data); + this.monitorList = data; + }); + + socket.on("updateMonitorIntoList", (data) => { + this.assignMonitorUrlParser(data); + Object.entries(data).forEach(([ monitorID, updatedMonitor ]) => { + this.monitorList[monitorID] = updatedMonitor; + }); + }); + + socket.on("deleteMonitorFromList", (monitorID) => { + if (this.monitorList[monitorID]) { + delete this.monitorList[monitorID]; + } + }); + + socket.on("monitorTypeList", (data) => { + this.monitorTypeList = data; + }); + + socket.on("maintenanceList", (data) => { + this.maintenanceList = data; + }); + + socket.on("apiKeyList", (data) => { + this.apiKeyList = data; + }); + + socket.on("notificationList", (data) => { + this.notificationList = data; + }); + + socket.on("statusPageList", (data) => { + this.statusPageListLoaded = true; + this.statusPageList = data; + }); + + socket.on("proxyList", (data) => { + this.proxyList = data.map(item => { + item.auth = !!item.auth; + item.active = !!item.active; + item.default = !!item.default; + + return item; + }); + }); + + socket.on("dockerHostList", (data) => { + this.dockerHostList = data; + }); + + socket.on("remoteBrowserList", (data) => { + this.remoteBrowserList = data; + }); + + socket.on("heartbeat", (data) => { + if (! (data.monitorID in this.heartbeatList)) { + this.heartbeatList[data.monitorID] = []; + } + + this.heartbeatList[data.monitorID].push(data); + + if (this.heartbeatList[data.monitorID].length >= 150) { + this.heartbeatList[data.monitorID].shift(); + } + + // Add to important list if it is important + // Also toast + if (data.important) { + + if (this.monitorList[data.monitorID] !== undefined) { + if (data.status === 0) { + toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, { + timeout: getToastErrorTimeout(), + }); + } else if (data.status === 1) { + toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, { + timeout: getToastSuccessTimeout(), + }); + } else { + toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); + } + } + + this.emitter.emit("newImportantHeartbeat", data); + } + }); + + socket.on("heartbeatList", (monitorID, data, overwrite = false) => { + if (! (monitorID in this.heartbeatList) || overwrite) { + this.heartbeatList[monitorID] = data; + } else { + this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]); + } + }); + + socket.on("avgPing", (monitorID, data) => { + this.avgPingList[monitorID] = data; + }); + + socket.on("uptime", (monitorID, type, data) => { + this.uptimeList[`${monitorID}_${type}`] = data; + }); + + socket.on("certInfo", (monitorID, data) => { + this.tlsInfoList[monitorID] = JSON.parse(data); + }); + + socket.on("connect_error", (err) => { + console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`); + this.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("Reconnecting...")}`; + this.showReverseProxyGuide = true; + this.socket.connected = false; + this.socket.firstConnect = false; + }); + + socket.on("disconnect", () => { + console.log("disconnect"); + this.connectionErrorMsg = `${this.$t("Lost connection to the socket server.")} ${this.$t("Reconnecting...")}`; + this.socket.connected = false; + }); + + socket.on("connect", () => { + console.log("Connected to the socket server"); + this.socket.connectCount++; + this.socket.connected = true; + this.showReverseProxyGuide = false; + + // Reset Heartbeat list if it is re-connect + if (this.socket.connectCount >= 2) { + this.clearData(); + } + + this.socket.firstConnect = false; + }); + + // cloudflared + socket.on("cloudflared_installed", (res) => this.cloudflared.installed = res); + socket.on("cloudflared_running", (res) => this.cloudflared.running = res); + socket.on("cloudflared_message", (res) => this.cloudflared.message = res); + socket.on("cloudflared_errorMessage", (res) => this.cloudflared.errorMessage = res); + socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res); + + socket.on("initServerTimezone", () => { + socket.emit("initServerTimezone", dayjs.tz.guess()); + }); + + socket.on("refresh", () => { + location.reload(); + }); + }, + /** + * parse all urls from list. + * @param {object} data Monitor data to modify + * @returns {object} list + */ + assignMonitorUrlParser(data) { + Object.entries(data).forEach(([ monitorID, monitor ]) => { + monitor.getUrl = () => { + try { + return new URL(monitor.url); + } catch (_) { + return null; + } + }; + }); + return data; + }, + + /** + * The storage currently in use + * @returns {Storage} Current storage + */ + storage() { + return (this.remember) ? localStorage : sessionStorage; + }, + + /** + * Get payload of JWT cookie + * @returns {(object | undefined)} JWT payload + */ + getJWTPayload() { + const jwtToken = this.$root.storage().token; + + if (jwtToken && jwtToken !== "autoLogin") { + return jwtDecode(jwtToken); + } + return undefined; + }, + + /** + * Get current socket + * @returns {Socket} Current socket + */ + getSocket() { + return socket; + }, + + /** + * Show success or error toast dependent on response status code + * @param {object} res Response object + * @returns {void} + */ + toastRes(res) { + let msg = res.msg; + if (res.msgi18n) { + if (msg != null && typeof msg === "object") { + msg = this.$t(msg.key, msg.values); + } else { + msg = this.$t(msg); + } + } + + if (res.ok) { + toast.success(msg); + } else { + toast.error(msg); + } + }, + + /** + * Show a success toast + * @param {string} msg Message to show + * @returns {void} + */ + toastSuccess(msg) { + toast.success(this.$t(msg)); + }, + + /** + * Show an error toast + * @param {string} msg Message to show + * @returns {void} + */ + toastError(msg) { + toast.error(this.$t(msg)); + }, + + /** + * Callback for login + * @callback loginCB + * @param {object} res Response object + */ + + /** + * Send request to log user in + * @param {string} username Username to log in with + * @param {string} password Password to log in with + * @param {string} token User token + * @param {loginCB} callback Callback to call with result + * @returns {void} + */ + login(username, password, token, callback) { + socket.emit("login", { + username, + password, + token, + }, (res) => { + if (res.tokenRequired) { + callback(res); + } + + if (res.ok) { + this.storage().token = res.token; + this.socket.token = res.token; + this.loggedIn = true; + this.username = this.getJWTPayload()?.username; + + // Trigger Chrome Save Password + history.pushState({}, ""); + } + + callback(res); + }); + }, + + /** + * Log in using a token + * @param {string} token Token to log in with + * @returns {void} + */ + loginByToken(token) { + socket.emit("loginByToken", token, (res) => { + this.allowLoginDialog = true; + + if (! res.ok) { + this.logout(); + } else { + this.loggedIn = true; + this.username = this.getJWTPayload()?.username; + } + }); + }, + + /** + * Log out of the web application + * @returns {void} + */ + logout() { + socket.emit("logout", () => { }); + this.storage().removeItem("token"); + this.socket.token = null; + this.loggedIn = false; + this.username = null; + this.clearData(); + }, + + /** + * Callback for general socket requests + * @callback socketCB + * @param {object} res Result of operation + */ + /** + * Prepare 2FA configuration + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + prepare2FA(callback) { + socket.emit("prepare2FA", callback); + }, + + /** + * Save the current 2FA configuration + * @param {any} secret Unused + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + save2FA(secret, callback) { + socket.emit("save2FA", callback); + }, + + /** + * Disable 2FA for this user + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + disable2FA(callback) { + socket.emit("disable2FA", callback); + }, + + /** + * Verify the provided 2FA token + * @param {string} token Token to verify + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + verifyToken(token, callback) { + socket.emit("verifyToken", token, callback); + }, + + /** + * Get current 2FA status + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + twoFAStatus(callback) { + socket.emit("twoFAStatus", callback); + }, + + /** + * Get list of monitors + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMonitorList(callback) { + if (! callback) { + callback = () => { }; + } + socket.emit("getMonitorList", callback); + }, + + /** + * Get list of maintenances + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMaintenanceList(callback) { + if (! callback) { + callback = () => { }; + } + socket.emit("getMaintenanceList", callback); + }, + + /** + * Send list of API keys + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getAPIKeyList(callback) { + if (!callback) { + callback = () => { }; + } + socket.emit("getAPIKeyList", callback); + }, + + /** + * Add a monitor + * @param {object} monitor Object representing monitor to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + add(monitor, callback) { + socket.emit("add", monitor, callback); + }, + + /** + * Adds a maintenance + * @param {object} maintenance Maintenance to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addMaintenance(maintenance, callback) { + socket.emit("addMaintenance", maintenance, callback); + }, + + /** + * Add monitors to maintenance + * @param {number} maintenanceID Maintenance to modify + * @param {number[]} monitors IDs of monitors to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addMonitorMaintenance(maintenanceID, monitors, callback) { + socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback); + }, + + /** + * Add status page to maintenance + * @param {number} maintenanceID Maintenance to modify + * @param {number} statusPages ID of status page to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addMaintenanceStatusPage(maintenanceID, statusPages, callback) { + socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback); + }, + + /** + * Get monitors affected by maintenance + * @param {number} maintenanceID Maintenance to read + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMonitorMaintenance(maintenanceID, callback) { + socket.emit("getMonitorMaintenance", maintenanceID, callback); + }, + + /** + * Get status pages where maintenance is shown + * @param {number} maintenanceID Maintenance to read + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMaintenanceStatusPage(maintenanceID, callback) { + socket.emit("getMaintenanceStatusPage", maintenanceID, callback); + }, + + /** + * Delete monitor by ID + * @param {number} monitorID ID of monitor to delete + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + deleteMonitor(monitorID, callback) { + socket.emit("deleteMonitor", monitorID, callback); + }, + + /** + * Delete specified maintenance + * @param {number} maintenanceID Maintenance to delete + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + deleteMaintenance(maintenanceID, callback) { + socket.emit("deleteMaintenance", maintenanceID, callback); + }, + + /** + * Add an API key + * @param {object} key API key to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addAPIKey(key, callback) { + socket.emit("addAPIKey", key, callback); + }, + + /** + * Delete specified API key + * @param {int} keyID ID of key to delete + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + deleteAPIKey(keyID, callback) { + socket.emit("deleteAPIKey", keyID, callback); + }, + + /** + * Clear the hearbeat list + * @returns {void} + */ + clearData() { + console.log("reset heartbeat list"); + this.heartbeatList = {}; + }, + + /** + * Upload the provided backup + * @param {string} uploadedJSON JSON to upload + * @param {string} importHandle Type of import. If set to + * most data in database will be replaced + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + uploadBackup(uploadedJSON, importHandle, callback) { + socket.emit("uploadBackup", uploadedJSON, importHandle, callback); + }, + + /** + * Clear events for a specified monitor + * @param {number} monitorID ID of monitor to clear + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + clearEvents(monitorID, callback) { + socket.emit("clearEvents", monitorID, callback); + }, + + /** + * Clear the heartbeats of a specified monitor + * @param {number} monitorID Id of monitor to clear + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + clearHeartbeats(monitorID, callback) { + socket.emit("clearHeartbeats", monitorID, callback); + }, + + /** + * Clear all statistics + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + clearStatistics(callback) { + socket.emit("clearStatistics", callback); + }, + + /** + * Get monitor beats for a specific monitor in a time range + * @param {number} monitorID ID of monitor to fetch + * @param {number} period Time in hours from now + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMonitorBeats(monitorID, period, callback) { + socket.emit("getMonitorBeats", monitorID, period, callback); + }, + + /** + * Retrieves monitor chart data. + * @param {string} monitorID - The ID of the monitor. + * @param {number} period - The time period for the chart data, in hours. + * @param {socketCB} callback - The callback function to handle the chart data. + * @returns {void} + */ + getMonitorChartData(monitorID, period, callback) { + socket.emit("getMonitorChartData", monitorID, period, callback); + } + }, + + computed: { + + usernameFirstChar() { + if (typeof this.username == "string" && this.username.length >= 1) { + return this.username.charAt(0).toUpperCase(); + } else { + return "🐻"; + } + }, + + lastHeartbeatList() { + let result = {}; + + for (let monitorID in this.heartbeatList) { + let index = this.heartbeatList[monitorID].length - 1; + result[monitorID] = this.heartbeatList[monitorID][index]; + } + + return result; + }, + + statusList() { + let result = {}; + + let unknown = { + text: this.$t("Unknown"), + color: "secondary", + }; + + for (let monitorID in this.lastHeartbeatList) { + let lastHeartBeat = this.lastHeartbeatList[monitorID]; + + if (! lastHeartBeat) { + result[monitorID] = unknown; + } else if (lastHeartBeat.status === UP) { + result[monitorID] = { + text: this.$t("Up"), + color: "primary", + }; + } else if (lastHeartBeat.status === DOWN) { + result[monitorID] = { + text: this.$t("Down"), + color: "danger", + }; + } else if (lastHeartBeat.status === PENDING) { + result[monitorID] = { + text: this.$t("Pending"), + color: "warning", + }; + } else if (lastHeartBeat.status === MAINTENANCE) { + result[monitorID] = { + text: this.$t("statusMaintenance"), + color: "maintenance", + }; + } else { + result[monitorID] = unknown; + } + } + + return result; + }, + + stats() { + let result = { + active: 0, + up: 0, + down: 0, + maintenance: 0, + pending: 0, + unknown: 0, + pause: 0, + }; + + for (let monitorID in this.$root.monitorList) { + let beat = this.$root.lastHeartbeatList[monitorID]; + let monitor = this.$root.monitorList[monitorID]; + + if (monitor && ! monitor.active) { + result.pause++; + } else if (beat) { + result.active++; + if (beat.status === UP) { + result.up++; + } else if (beat.status === DOWN) { + result.down++; + } else if (beat.status === PENDING) { + result.pending++; + } else if (beat.status === MAINTENANCE) { + result.maintenance++; + } else { + result.unknown++; + } + } else { + result.unknown++; + } + } + + return result; + }, + + /** + * Frontend Version + * It should be compiled to a static value while building the frontend. + * Please see ./config/vite.config.js, it is defined via vite.js + * @returns {string} Current version + */ + frontendVersion() { + // eslint-disable-next-line no-undef + return FRONTEND_VERSION; + }, + + /** + * Are both frontend and backend in the same version? + * @returns {boolean} The frontend and backend match? + */ + isFrontendBackendVersionMatched() { + if (!this.info.version) { + return true; + } + return this.info.version === this.frontendVersion; + } + }, + + watch: { + + // Update Badge + "stats.down"(to, from) { + if (to !== from) { + if (this.faviconUpdateDebounce != null) { + clearTimeout(this.faviconUpdateDebounce); + } + this.faviconUpdateDebounce = setTimeout(() => { + favicon.badge(to); + }, 1000); + } + }, + + // Reload the SPA if the server version is changed. + "info.version"(to, from) { + if (from && from !== to) { + window.location.reload(); + } + }, + + remember() { + localStorage.remember = (this.remember) ? "1" : "0"; + }, + + // Reconnect the socket io, if status-page to dashboard + "$route.fullPath"(newValue, oldValue) { + + if (newValue) { + for (let page of noSocketIOPages) { + if (newValue.match(page)) { + return; + } + } + } + + this.initSocketIO(); + }, + + }, + +}; |