Browse Source
			
			
			
			
				
		Farsi Lang Updated Some hardcoded words has been replaced with translationspull/487/head
				 68 changed files with 4157 additions and 1274 deletions
			
			
		
								
									Binary file not shown.
								
							
						
					@ -0,0 +1,30 @@ | 
				
			|||||
 | 
					-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | 
				
			||||
 | 
					BEGIN TRANSACTION; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					create table `group` | 
				
			||||
 | 
					( | 
				
			||||
 | 
					    id           INTEGER      not null | 
				
			||||
 | 
					        constraint group_pk | 
				
			||||
 | 
					            primary key autoincrement, | 
				
			||||
 | 
					    name         VARCHAR(255) not null, | 
				
			||||
 | 
					    created_date DATETIME              default (DATETIME('now')) not null, | 
				
			||||
 | 
					    public       BOOLEAN               default 0 not null, | 
				
			||||
 | 
					    active       BOOLEAN               default 1 not null, | 
				
			||||
 | 
					    weight       BOOLEAN      NOT NULL DEFAULT 1000 | 
				
			||||
 | 
					); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					CREATE TABLE [monitor_group] | 
				
			||||
 | 
					( | 
				
			||||
 | 
					    [id]         INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | 
				
			||||
 | 
					    [monitor_id] INTEGER                           NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | 
				
			||||
 | 
					    [group_id]   INTEGER                           NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | 
				
			||||
 | 
					    weight BOOLEAN NOT NULL DEFAULT 1000 | 
				
			||||
 | 
					); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					CREATE INDEX [fk] | 
				
			||||
 | 
					    ON [monitor_group] ( | 
				
			||||
 | 
					                        [monitor_id], | 
				
			||||
 | 
					                        [group_id]); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					COMMIT; | 
				
			||||
@ -0,0 +1,18 @@ | 
				
			|||||
 | 
					-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | 
				
			||||
 | 
					BEGIN TRANSACTION; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					create table incident | 
				
			||||
 | 
					( | 
				
			||||
 | 
					    id INTEGER not null | 
				
			||||
 | 
					        constraint incident_pk | 
				
			||||
 | 
					            primary key autoincrement, | 
				
			||||
 | 
					    title VARCHAR(255) not null, | 
				
			||||
 | 
					    content TEXT not null, | 
				
			||||
 | 
					    style VARCHAR(30) default 'warning' not null, | 
				
			||||
 | 
					    created_date DATETIME default (DATETIME('now')) not null, | 
				
			||||
 | 
					    last_updated_date DATETIME, | 
				
			||||
 | 
					    pin BOOLEAN default 1 not null, | 
				
			||||
 | 
					    active BOOLEAN default 1 not null | 
				
			||||
 | 
					); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					COMMIT; | 
				
			||||
								
									
										File diff suppressed because it is too large
									
								
							
						
					@ -0,0 +1,57 @@ | 
				
			|||||
 | 
					/* | 
				
			||||
 | 
					    From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
 | 
				
			||||
 | 
					    Modified with 0 dependencies | 
				
			||||
 | 
					 */ | 
				
			||||
 | 
					let fs = require("fs"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let ImageDataURI = (() => { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function decode(dataURI) { | 
				
			||||
 | 
					        if (!/data:image\//.test(dataURI)) { | 
				
			||||
 | 
					            console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); | 
				
			||||
 | 
					            return null; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); | 
				
			||||
 | 
					        return { | 
				
			||||
 | 
					            imageType: regExMatches[1], | 
				
			||||
 | 
					            dataBase64: regExMatches[2], | 
				
			||||
 | 
					            dataBuffer: new Buffer(regExMatches[2], "base64") | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function encode(data, mediaType) { | 
				
			||||
 | 
					        if (!data || !mediaType) { | 
				
			||||
 | 
					            console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); | 
				
			||||
 | 
					            return null; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; | 
				
			||||
 | 
					        let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); | 
				
			||||
 | 
					        let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return dataImgBase64; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function outputFile(dataURI, filePath) { | 
				
			||||
 | 
					        filePath = filePath || "./"; | 
				
			||||
 | 
					        return new Promise((resolve, reject) => { | 
				
			||||
 | 
					            let imageDecoded = decode(dataURI); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            fs.writeFile(filePath, imageDecoded.dataBuffer, err => { | 
				
			||||
 | 
					                if (err) { | 
				
			||||
 | 
					                    return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					                resolve(filePath); | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    return { | 
				
			||||
 | 
					        decode: decode, | 
				
			||||
 | 
					        encode: encode, | 
				
			||||
 | 
					        outputFile: outputFile, | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					})(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports = ImageDataURI; | 
				
			||||
@ -0,0 +1,34 @@ | 
				
			|||||
 | 
					const { BeanModel } = require("redbean-node/dist/bean-model"); | 
				
			||||
 | 
					const { R } = require("redbean-node"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					class Group extends BeanModel { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    async toPublicJSON() { | 
				
			||||
 | 
					        let monitorBeanList = await this.getMonitorList(); | 
				
			||||
 | 
					        let monitorList = []; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        for (let bean of monitorBeanList) { | 
				
			||||
 | 
					            monitorList.push(await bean.toPublicJSON()); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return { | 
				
			||||
 | 
					            id: this.id, | 
				
			||||
 | 
					            name: this.name, | 
				
			||||
 | 
					            weight: this.weight, | 
				
			||||
 | 
					            monitorList, | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    async getMonitorList() { | 
				
			||||
 | 
					        return R.convertToBeans("monitor", await R.getAll(` | 
				
			||||
 | 
					            SELECT monitor.* FROM monitor, monitor_group | 
				
			||||
 | 
					            WHERE monitor.id = monitor_group.monitor_id | 
				
			||||
 | 
					            AND group_id = ? | 
				
			||||
 | 
					            ORDER BY monitor_group.weight | 
				
			||||
 | 
					        `, [
 | 
				
			||||
 | 
					            this.id, | 
				
			||||
 | 
					        ])); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports = Group; | 
				
			||||
@ -0,0 +1,18 @@ | 
				
			|||||
 | 
					const { BeanModel } = require("redbean-node/dist/bean-model"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					class Incident extends BeanModel { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    toPublicJSON() { | 
				
			||||
 | 
					        return { | 
				
			||||
 | 
					            id: this.id, | 
				
			||||
 | 
					            style: this.style, | 
				
			||||
 | 
					            title: this.title, | 
				
			||||
 | 
					            content: this.content, | 
				
			||||
 | 
					            pin: this.pin, | 
				
			||||
 | 
					            createdDate: this.createdDate, | 
				
			||||
 | 
					            lastUpdatedDate: this.lastUpdatedDate, | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports = Incident; | 
				
			||||
@ -0,0 +1,749 @@ | 
				
			|||||
 | 
					let url = require("url"); | 
				
			||||
 | 
					let MemoryCache = require("./memory-cache"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let t = { | 
				
			||||
 | 
					    ms: 1, | 
				
			||||
 | 
					    second: 1000, | 
				
			||||
 | 
					    minute: 60000, | 
				
			||||
 | 
					    hour: 3600000, | 
				
			||||
 | 
					    day: 3600000 * 24, | 
				
			||||
 | 
					    week: 3600000 * 24 * 7, | 
				
			||||
 | 
					    month: 3600000 * 24 * 30, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let instances = []; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let matches = function (a) { | 
				
			||||
 | 
					    return function (b) { | 
				
			||||
 | 
					        return a === b; | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let doesntMatch = function (a) { | 
				
			||||
 | 
					    return function (b) { | 
				
			||||
 | 
					        return !matches(a)(b); | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let logDuration = function (d, prefix) { | 
				
			||||
 | 
					    let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms"; | 
				
			||||
 | 
					    return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m"; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					function getSafeHeaders(res) { | 
				
			||||
 | 
					    return res.getHeaders ? res.getHeaders() : res._headers; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					function ApiCache() { | 
				
			||||
 | 
					    let memCache = new MemoryCache(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let globalOptions = { | 
				
			||||
 | 
					        debug: false, | 
				
			||||
 | 
					        defaultDuration: 3600000, | 
				
			||||
 | 
					        enabled: true, | 
				
			||||
 | 
					        appendKey: [], | 
				
			||||
 | 
					        jsonp: false, | 
				
			||||
 | 
					        redisClient: false, | 
				
			||||
 | 
					        headerBlacklist: [], | 
				
			||||
 | 
					        statusCodes: { | 
				
			||||
 | 
					            include: [], | 
				
			||||
 | 
					            exclude: [], | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					        events: { | 
				
			||||
 | 
					            expire: undefined, | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					        headers: { | 
				
			||||
 | 
					            // 'cache-control':  'no-cache' // example of header overwrite
 | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					        trackPerformance: false, | 
				
			||||
 | 
					        respectCacheControl: false, | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let middlewareOptions = []; | 
				
			||||
 | 
					    let instance = this; | 
				
			||||
 | 
					    let index = null; | 
				
			||||
 | 
					    let timers = {}; | 
				
			||||
 | 
					    let performanceArray = []; // for tracking cache hit rate
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    instances.push(this); | 
				
			||||
 | 
					    this.id = instances.length; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function debug(a, b, c, d) { | 
				
			||||
 | 
					        let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) { | 
				
			||||
 | 
					            return arg !== undefined; | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					        let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return (globalOptions.debug || debugEnv) && console.log.apply(null, arr); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function shouldCacheResponse(request, response, toggle) { | 
				
			||||
 | 
					        let opt = globalOptions; | 
				
			||||
 | 
					        let codes = opt.statusCodes; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (!response) { | 
				
			||||
 | 
					            return false; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (toggle && !toggle(request, response)) { | 
				
			||||
 | 
					            return false; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) { | 
				
			||||
 | 
					            return false; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) { | 
				
			||||
 | 
					            return false; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return true; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function addIndexEntries(key, req) { | 
				
			||||
 | 
					        let groupName = req.apicacheGroup; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (groupName) { | 
				
			||||
 | 
					            debug("group detected \"" + groupName + "\""); | 
				
			||||
 | 
					            let group = (index.groups[groupName] = index.groups[groupName] || []); | 
				
			||||
 | 
					            group.unshift(key); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        index.all.unshift(key); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function filterBlacklistedHeaders(headers) { | 
				
			||||
 | 
					        return Object.keys(headers) | 
				
			||||
 | 
					            .filter(function (key) { | 
				
			||||
 | 
					                return globalOptions.headerBlacklist.indexOf(key) === -1; | 
				
			||||
 | 
					            }) | 
				
			||||
 | 
					            .reduce(function (acc, header) { | 
				
			||||
 | 
					                acc[header] = headers[header]; | 
				
			||||
 | 
					                return acc; | 
				
			||||
 | 
					            }, {}); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function createCacheObject(status, headers, data, encoding) { | 
				
			||||
 | 
					        return { | 
				
			||||
 | 
					            status: status, | 
				
			||||
 | 
					            headers: filterBlacklistedHeaders(headers), | 
				
			||||
 | 
					            data: data, | 
				
			||||
 | 
					            encoding: encoding, | 
				
			||||
 | 
					            timestamp: new Date().getTime() / 1000, // seconds since epoch.  This is used to properly decrement max-age headers in cached responses.
 | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function cacheResponse(key, value, duration) { | 
				
			||||
 | 
					        let redis = globalOptions.redisClient; | 
				
			||||
 | 
					        let expireCallback = globalOptions.events.expire; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (redis && redis.connected) { | 
				
			||||
 | 
					            try { | 
				
			||||
 | 
					                redis.hset(key, "response", JSON.stringify(value)); | 
				
			||||
 | 
					                redis.hset(key, "duration", duration); | 
				
			||||
 | 
					                redis.expire(key, duration / 1000, expireCallback || function () {}); | 
				
			||||
 | 
					            } catch (err) { | 
				
			||||
 | 
					                debug("[apicache] error in redis.hset()"); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } else { | 
				
			||||
 | 
					            memCache.add(key, value, duration, expireCallback); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // add automatic cache clearing from duration, includes max limit on setTimeout
 | 
				
			||||
 | 
					        timers[key] = setTimeout(function () { | 
				
			||||
 | 
					            instance.clear(key, true); | 
				
			||||
 | 
					        }, Math.min(duration, 2147483647)); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function accumulateContent(res, content) { | 
				
			||||
 | 
					        if (content) { | 
				
			||||
 | 
					            if (typeof content == "string") { | 
				
			||||
 | 
					                res._apicache.content = (res._apicache.content || "") + content; | 
				
			||||
 | 
					            } else if (Buffer.isBuffer(content)) { | 
				
			||||
 | 
					                let oldContent = res._apicache.content; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                if (typeof oldContent === "string") { | 
				
			||||
 | 
					                    oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                if (!oldContent) { | 
				
			||||
 | 
					                    oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                res._apicache.content = Buffer.concat( | 
				
			||||
 | 
					                    [oldContent, content], | 
				
			||||
 | 
					                    oldContent.length + content.length | 
				
			||||
 | 
					                ); | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                res._apicache.content = content; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) { | 
				
			||||
 | 
					    // monkeypatch res.end to create cache object
 | 
				
			||||
 | 
					        res._apicache = { | 
				
			||||
 | 
					            write: res.write, | 
				
			||||
 | 
					            writeHead: res.writeHead, | 
				
			||||
 | 
					            end: res.end, | 
				
			||||
 | 
					            cacheable: true, | 
				
			||||
 | 
					            content: undefined, | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // append header overwrites if applicable
 | 
				
			||||
 | 
					        Object.keys(globalOptions.headers).forEach(function (name) { | 
				
			||||
 | 
					            res.setHeader(name, globalOptions.headers[name]); | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        res.writeHead = function () { | 
				
			||||
 | 
					            // add cache control headers
 | 
				
			||||
 | 
					            if (!globalOptions.headers["cache-control"]) { | 
				
			||||
 | 
					                if (shouldCacheResponse(req, res, toggle)) { | 
				
			||||
 | 
					                    res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0)); | 
				
			||||
 | 
					                } else { | 
				
			||||
 | 
					                    res.setHeader("cache-control", "no-cache, no-store, must-revalidate"); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            res._apicache.headers = Object.assign({}, getSafeHeaders(res)); | 
				
			||||
 | 
					            return res._apicache.writeHead.apply(this, arguments); | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // patch res.write
 | 
				
			||||
 | 
					        res.write = function (content) { | 
				
			||||
 | 
					            accumulateContent(res, content); | 
				
			||||
 | 
					            return res._apicache.write.apply(this, arguments); | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // patch res.end
 | 
				
			||||
 | 
					        res.end = function (content, encoding) { | 
				
			||||
 | 
					            if (shouldCacheResponse(req, res, toggle)) { | 
				
			||||
 | 
					                accumulateContent(res, content); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                if (res._apicache.cacheable && res._apicache.content) { | 
				
			||||
 | 
					                    addIndexEntries(key, req); | 
				
			||||
 | 
					                    let headers = res._apicache.headers || getSafeHeaders(res); | 
				
			||||
 | 
					                    let cacheObject = createCacheObject( | 
				
			||||
 | 
					                        res.statusCode, | 
				
			||||
 | 
					                        headers, | 
				
			||||
 | 
					                        res._apicache.content, | 
				
			||||
 | 
					                        encoding | 
				
			||||
 | 
					                    ); | 
				
			||||
 | 
					                    cacheResponse(key, cacheObject, duration); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                    // display log entry
 | 
				
			||||
 | 
					                    let elapsed = new Date() - req.apicacheTimer; | 
				
			||||
 | 
					                    debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed)); | 
				
			||||
 | 
					                    debug("_apicache.headers: ", res._apicache.headers); | 
				
			||||
 | 
					                    debug("res.getHeaders(): ", getSafeHeaders(res)); | 
				
			||||
 | 
					                    debug("cacheObject: ", cacheObject); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            return res._apicache.end.apply(this, arguments); | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        next(); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function sendCachedResponse(request, response, cacheObject, toggle, next, duration) { | 
				
			||||
 | 
					        if (toggle && !toggle(request, response)) { | 
				
			||||
 | 
					            return next(); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let headers = getSafeHeaders(response); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // Modified by @louislam, removed Cache-control, since I don't need client side cache!
 | 
				
			||||
 | 
					        // Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
 | 
				
			||||
 | 
					        Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {})); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // only embed apicache headers when not in production environment
 | 
				
			||||
 | 
					        if (process.env.NODE_ENV !== "production") { | 
				
			||||
 | 
					            Object.assign(headers, { | 
				
			||||
 | 
					                "apicache-store": globalOptions.redisClient ? "redis" : "memory", | 
				
			||||
 | 
					                "apicache-version": "1.6.2-modified", | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // unstringify buffers
 | 
				
			||||
 | 
					        let data = cacheObject.data; | 
				
			||||
 | 
					        if (data && data.type === "Buffer") { | 
				
			||||
 | 
					            data = | 
				
			||||
 | 
					        typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // test Etag against If-None-Match for 304
 | 
				
			||||
 | 
					        let cachedEtag = cacheObject.headers.etag; | 
				
			||||
 | 
					        let requestEtag = request.headers["if-none-match"]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (requestEtag && cachedEtag === requestEtag) { | 
				
			||||
 | 
					            response.writeHead(304, headers); | 
				
			||||
 | 
					            return response.end(); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        response.writeHead(cacheObject.status || 200, headers); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return response.end(data, cacheObject.encoding); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function syncOptions() { | 
				
			||||
 | 
					        for (let i in middlewareOptions) { | 
				
			||||
 | 
					            Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.clear = function (target, isAutomatic) { | 
				
			||||
 | 
					        let group = index.groups[target]; | 
				
			||||
 | 
					        let redis = globalOptions.redisClient; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (group) { | 
				
			||||
 | 
					            debug("clearing group \"" + target + "\""); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            group.forEach(function (key) { | 
				
			||||
 | 
					                debug("clearing cached entry for \"" + key + "\""); | 
				
			||||
 | 
					                clearTimeout(timers[key]); | 
				
			||||
 | 
					                delete timers[key]; | 
				
			||||
 | 
					                if (!globalOptions.redisClient) { | 
				
			||||
 | 
					                    memCache.delete(key); | 
				
			||||
 | 
					                } else { | 
				
			||||
 | 
					                    try { | 
				
			||||
 | 
					                        redis.del(key); | 
				
			||||
 | 
					                    } catch (err) { | 
				
			||||
 | 
					                        console.log("[apicache] error in redis.del(\"" + key + "\")"); | 
				
			||||
 | 
					                    } | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					                index.all = index.all.filter(doesntMatch(key)); | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            delete index.groups[target]; | 
				
			||||
 | 
					        } else if (target) { | 
				
			||||
 | 
					            debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\""); | 
				
			||||
 | 
					            clearTimeout(timers[target]); | 
				
			||||
 | 
					            delete timers[target]; | 
				
			||||
 | 
					            // clear actual cached entry
 | 
				
			||||
 | 
					            if (!redis) { | 
				
			||||
 | 
					                memCache.delete(target); | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                try { | 
				
			||||
 | 
					                    redis.del(target); | 
				
			||||
 | 
					                } catch (err) { | 
				
			||||
 | 
					                    console.log("[apicache] error in redis.del(\"" + target + "\")"); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // remove from global index
 | 
				
			||||
 | 
					            index.all = index.all.filter(doesntMatch(target)); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // remove target from each group that it may exist in
 | 
				
			||||
 | 
					            Object.keys(index.groups).forEach(function (groupName) { | 
				
			||||
 | 
					                index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target)); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                // delete group if now empty
 | 
				
			||||
 | 
					                if (!index.groups[groupName].length) { | 
				
			||||
 | 
					                    delete index.groups[groupName]; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } else { | 
				
			||||
 | 
					            debug("clearing entire index"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (!redis) { | 
				
			||||
 | 
					                memCache.clear(); | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                // clear redis keys one by one from internal index to prevent clearing non-apicache entries
 | 
				
			||||
 | 
					                index.all.forEach(function (key) { | 
				
			||||
 | 
					                    clearTimeout(timers[key]); | 
				
			||||
 | 
					                    delete timers[key]; | 
				
			||||
 | 
					                    try { | 
				
			||||
 | 
					                        redis.del(key); | 
				
			||||
 | 
					                    } catch (err) { | 
				
			||||
 | 
					                        console.log("[apicache] error in redis.del(\"" + key + "\")"); | 
				
			||||
 | 
					                    } | 
				
			||||
 | 
					                }); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            this.resetIndex(); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return this.getIndex(); | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    function parseDuration(duration, defaultDuration) { | 
				
			||||
 | 
					        if (typeof duration === "number") { | 
				
			||||
 | 
					            return duration; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (typeof duration === "string") { | 
				
			||||
 | 
					            let split = duration.match(/^([\d\.,]+)\s?(\w+)$/); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (split.length === 3) { | 
				
			||||
 | 
					                let len = parseFloat(split[1]); | 
				
			||||
 | 
					                let unit = split[2].replace(/s$/i, "").toLowerCase(); | 
				
			||||
 | 
					                if (unit === "m") { | 
				
			||||
 | 
					                    unit = "ms"; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                return (len || 1) * (t[unit] || 0); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return defaultDuration; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.getDuration = function (duration) { | 
				
			||||
 | 
					        return parseDuration(duration, globalOptions.defaultDuration); | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    /** | 
				
			||||
 | 
					   * Return cache performance statistics (hit rate).  Suitable for putting into a route: | 
				
			||||
 | 
					   * <code> | 
				
			||||
 | 
					   * app.get('/api/cache/performance', (req, res) => { | 
				
			||||
 | 
					   *    res.json(apicache.getPerformance()) | 
				
			||||
 | 
					   * }) | 
				
			||||
 | 
					   * </code> | 
				
			||||
 | 
					   */ | 
				
			||||
 | 
					    this.getPerformance = function () { | 
				
			||||
 | 
					        return performanceArray.map(function (p) { | 
				
			||||
 | 
					            return p.report(); | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.getIndex = function (group) { | 
				
			||||
 | 
					        if (group) { | 
				
			||||
 | 
					            return index.groups[group]; | 
				
			||||
 | 
					        } else { | 
				
			||||
 | 
					            return index; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.middleware = function cache(strDuration, middlewareToggle, localOptions) { | 
				
			||||
 | 
					        let duration = instance.getDuration(strDuration); | 
				
			||||
 | 
					        let opt = {}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        middlewareOptions.push({ | 
				
			||||
 | 
					            options: opt, | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let options = function (localOptions) { | 
				
			||||
 | 
					            if (localOptions) { | 
				
			||||
 | 
					                middlewareOptions.find(function (middleware) { | 
				
			||||
 | 
					                    return middleware.options === opt; | 
				
			||||
 | 
					                }).localOptions = localOptions; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            syncOptions(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            return opt; | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        options(localOptions); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        /** | 
				
			||||
 | 
					     * A Function for non tracking performance | 
				
			||||
 | 
					     */ | 
				
			||||
 | 
					        function NOOPCachePerformance() { | 
				
			||||
 | 
					            this.report = this.hit = this.miss = function () {}; // noop;
 | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        /** | 
				
			||||
 | 
					     * A function for tracking and reporting hit rate.  These statistics are returned by the getPerformance() call above. | 
				
			||||
 | 
					     */ | 
				
			||||
 | 
					        function CachePerformance() { | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Tracks the hit rate for the last 100 requests. | 
				
			||||
 | 
					       * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened. | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Tracks the hit rate for the last 1000 requests. | 
				
			||||
 | 
					       * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened. | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Tracks the hit rate for the last 10000 requests. | 
				
			||||
 | 
					       * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened. | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Tracks the hit rate for the last 100000 requests. | 
				
			||||
 | 
					       * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened. | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * The number of calls that have passed through the middleware since the server started. | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.callCount = 0; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * The total number of hits since the server started | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.hitCount = 0; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * The key from the last cache hit.  This is useful in identifying which route these statistics apply to. | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.lastCacheHit = null; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * The key from the last cache miss.  This is useful in identifying which route these statistics apply to. | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.lastCacheMiss = null; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Return performance statistics | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.report = function () { | 
				
			||||
 | 
					                return { | 
				
			||||
 | 
					                    lastCacheHit: this.lastCacheHit, | 
				
			||||
 | 
					                    lastCacheMiss: this.lastCacheMiss, | 
				
			||||
 | 
					                    callCount: this.callCount, | 
				
			||||
 | 
					                    hitCount: this.hitCount, | 
				
			||||
 | 
					                    missCount: this.callCount - this.hitCount, | 
				
			||||
 | 
					                    hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount, | 
				
			||||
 | 
					                    hitRateLast100: this.hitRate(this.hitsLast100), | 
				
			||||
 | 
					                    hitRateLast1000: this.hitRate(this.hitsLast1000), | 
				
			||||
 | 
					                    hitRateLast10000: this.hitRate(this.hitsLast10000), | 
				
			||||
 | 
					                    hitRateLast100000: this.hitRate(this.hitsLast100000), | 
				
			||||
 | 
					                }; | 
				
			||||
 | 
					            }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Computes a cache hit rate from an array of hits and misses. | 
				
			||||
 | 
					       * @param {Uint8Array} array An array representing hits and misses. | 
				
			||||
 | 
					       * @returns a number between 0 and 1, or null if the array has no hits or misses | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.hitRate = function (array) { | 
				
			||||
 | 
					                let hits = 0; | 
				
			||||
 | 
					                let misses = 0; | 
				
			||||
 | 
					                for (let i = 0; i < array.length; i++) { | 
				
			||||
 | 
					                    let n8 = array[i]; | 
				
			||||
 | 
					                    for (let j = 0; j < 4; j++) { | 
				
			||||
 | 
					                        switch (n8 & 3) { | 
				
			||||
 | 
					                            case 1: | 
				
			||||
 | 
					                                hits++; | 
				
			||||
 | 
					                                break; | 
				
			||||
 | 
					                            case 2: | 
				
			||||
 | 
					                                misses++; | 
				
			||||
 | 
					                                break; | 
				
			||||
 | 
					                        } | 
				
			||||
 | 
					                        n8 >>= 2; | 
				
			||||
 | 
					                    } | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					                let total = hits + misses; | 
				
			||||
 | 
					                if (total == 0) { | 
				
			||||
 | 
					                    return null; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					                return hits / total; | 
				
			||||
 | 
					            }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Record a hit or miss in the given array.  It will be recorded at a position determined | 
				
			||||
 | 
					       * by the current value of the callCount variable. | 
				
			||||
 | 
					       * @param {Uint8Array} array An array representing hits and misses. | 
				
			||||
 | 
					       * @param {boolean} hit true for a hit, false for a miss | 
				
			||||
 | 
					       * Each element in the array is 8 bits, and encodes 4 hit/miss records. | 
				
			||||
 | 
					       * Each hit or miss is encoded as to bits as follows: | 
				
			||||
 | 
					       * 00 means no hit or miss has been recorded in these bits | 
				
			||||
 | 
					       * 01 encodes a hit | 
				
			||||
 | 
					       * 10 encodes a miss | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.recordHitInArray = function (array, hit) { | 
				
			||||
 | 
					                let arrayIndex = ~~(this.callCount / 4) % array.length; | 
				
			||||
 | 
					                let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
 | 
				
			||||
 | 
					                let clearMask = ~(3 << bitOffset); | 
				
			||||
 | 
					                let record = (hit ? 1 : 2) << bitOffset; | 
				
			||||
 | 
					                array[arrayIndex] = (array[arrayIndex] & clearMask) | record; | 
				
			||||
 | 
					            }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Records the hit or miss in the tracking arrays and increments the call count. | 
				
			||||
 | 
					       * @param {boolean} hit true records a hit, false records a miss | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.recordHit = function (hit) { | 
				
			||||
 | 
					                this.recordHitInArray(this.hitsLast100, hit); | 
				
			||||
 | 
					                this.recordHitInArray(this.hitsLast1000, hit); | 
				
			||||
 | 
					                this.recordHitInArray(this.hitsLast10000, hit); | 
				
			||||
 | 
					                this.recordHitInArray(this.hitsLast100000, hit); | 
				
			||||
 | 
					                if (hit) { | 
				
			||||
 | 
					                    this.hitCount++; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					                this.callCount++; | 
				
			||||
 | 
					            }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Records a hit event, setting lastCacheMiss to the given key | 
				
			||||
 | 
					       * @param {string} key The key that had the cache hit | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.hit = function (key) { | 
				
			||||
 | 
					                this.recordHit(true); | 
				
			||||
 | 
					                this.lastCacheHit = key; | 
				
			||||
 | 
					            }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            /** | 
				
			||||
 | 
					       * Records a miss event, setting lastCacheMiss to the given key | 
				
			||||
 | 
					       * @param {string} key The key that had the cache miss | 
				
			||||
 | 
					       */ | 
				
			||||
 | 
					            this.miss = function (key) { | 
				
			||||
 | 
					                this.recordHit(false); | 
				
			||||
 | 
					                this.lastCacheMiss = key; | 
				
			||||
 | 
					            }; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        performanceArray.push(perf); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let cache = function (req, res, next) { | 
				
			||||
 | 
					            function bypass() { | 
				
			||||
 | 
					                debug("bypass detected, skipping cache."); | 
				
			||||
 | 
					                return next(); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // initial bypass chances
 | 
				
			||||
 | 
					            if (!opt.enabled) { | 
				
			||||
 | 
					                return bypass(); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            if ( | 
				
			||||
 | 
					                req.headers["x-apicache-bypass"] || | 
				
			||||
 | 
					        req.headers["x-apicache-force-fetch"] || | 
				
			||||
 | 
					        (opt.respectCacheControl && req.headers["cache-control"] == "no-cache") | 
				
			||||
 | 
					            ) { | 
				
			||||
 | 
					                return bypass(); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
 | 
				
			||||
 | 
					            // if (typeof middlewareToggle === 'function') {
 | 
				
			||||
 | 
					            //   if (!middlewareToggle(req, res)) return bypass()
 | 
				
			||||
 | 
					            // } else if (middlewareToggle !== undefined && !middlewareToggle) {
 | 
				
			||||
 | 
					            //   return bypass()
 | 
				
			||||
 | 
					            // }
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // embed timer
 | 
				
			||||
 | 
					            req.apicacheTimer = new Date(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // In Express 4.x the url is ambigious based on where a router is mounted.  originalUrl will give the full Url
 | 
				
			||||
 | 
					            let key = req.originalUrl || req.url; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // Remove querystring from key if jsonp option is enabled
 | 
				
			||||
 | 
					            if (opt.jsonp) { | 
				
			||||
 | 
					                key = url.parse(key).pathname; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // add appendKey (either custom function or response path)
 | 
				
			||||
 | 
					            if (typeof opt.appendKey === "function") { | 
				
			||||
 | 
					                key += "$$appendKey=" + opt.appendKey(req, res); | 
				
			||||
 | 
					            } else if (opt.appendKey.length > 0) { | 
				
			||||
 | 
					                let appendKey = req; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                for (let i = 0; i < opt.appendKey.length; i++) { | 
				
			||||
 | 
					                    appendKey = appendKey[opt.appendKey[i]]; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					                key += "$$appendKey=" + appendKey; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // attempt cache hit
 | 
				
			||||
 | 
					            let redis = opt.redisClient; | 
				
			||||
 | 
					            let cached = !redis ? memCache.getValue(key) : null; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // send if cache hit from memory-cache
 | 
				
			||||
 | 
					            if (cached) { | 
				
			||||
 | 
					                let elapsed = new Date() - req.apicacheTimer; | 
				
			||||
 | 
					                debug("sending cached (memory-cache) version of", key, logDuration(elapsed)); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                perf.hit(key); | 
				
			||||
 | 
					                return sendCachedResponse(req, res, cached, middlewareToggle, next, duration); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // send if cache hit from redis
 | 
				
			||||
 | 
					            if (redis && redis.connected) { | 
				
			||||
 | 
					                try { | 
				
			||||
 | 
					                    redis.hgetall(key, function (err, obj) { | 
				
			||||
 | 
					                        if (!err && obj && obj.response) { | 
				
			||||
 | 
					                            let elapsed = new Date() - req.apicacheTimer; | 
				
			||||
 | 
					                            debug("sending cached (redis) version of", key, logDuration(elapsed)); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                            perf.hit(key); | 
				
			||||
 | 
					                            return sendCachedResponse( | 
				
			||||
 | 
					                                req, | 
				
			||||
 | 
					                                res, | 
				
			||||
 | 
					                                JSON.parse(obj.response), | 
				
			||||
 | 
					                                middlewareToggle, | 
				
			||||
 | 
					                                next, | 
				
			||||
 | 
					                                duration | 
				
			||||
 | 
					                            ); | 
				
			||||
 | 
					                        } else { | 
				
			||||
 | 
					                            perf.miss(key); | 
				
			||||
 | 
					                            return makeResponseCacheable( | 
				
			||||
 | 
					                                req, | 
				
			||||
 | 
					                                res, | 
				
			||||
 | 
					                                next, | 
				
			||||
 | 
					                                key, | 
				
			||||
 | 
					                                duration, | 
				
			||||
 | 
					                                strDuration, | 
				
			||||
 | 
					                                middlewareToggle | 
				
			||||
 | 
					                            ); | 
				
			||||
 | 
					                        } | 
				
			||||
 | 
					                    }); | 
				
			||||
 | 
					                } catch (err) { | 
				
			||||
 | 
					                    // bypass redis on error
 | 
				
			||||
 | 
					                    perf.miss(key); | 
				
			||||
 | 
					                    return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                perf.miss(key); | 
				
			||||
 | 
					                return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        cache.options = options; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return cache; | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.options = function (options) { | 
				
			||||
 | 
					        if (options) { | 
				
			||||
 | 
					            Object.assign(globalOptions, options); | 
				
			||||
 | 
					            syncOptions(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if ("defaultDuration" in options) { | 
				
			||||
 | 
					                // Convert the default duration to a number in milliseconds (if needed)
 | 
				
			||||
 | 
					                globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (globalOptions.trackPerformance) { | 
				
			||||
 | 
					                debug("WARNING: using trackPerformance flag can cause high memory usage!"); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            return this; | 
				
			||||
 | 
					        } else { | 
				
			||||
 | 
					            return globalOptions; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.resetIndex = function () { | 
				
			||||
 | 
					        index = { | 
				
			||||
 | 
					            all: [], | 
				
			||||
 | 
					            groups: {}, | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.newInstance = function (config) { | 
				
			||||
 | 
					        let instance = new ApiCache(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (config) { | 
				
			||||
 | 
					            instance.options(config); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        return instance; | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.clone = function () { | 
				
			||||
 | 
					        return this.newInstance(this.options()); | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // initialize index
 | 
				
			||||
 | 
					    this.resetIndex(); | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports = new ApiCache(); | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			|||||
 | 
					const apicache = require("./apicache"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					apicache.options({ | 
				
			||||
 | 
					    headerBlacklist: [ | 
				
			||||
 | 
					        "cache-control" | 
				
			||||
 | 
					    ], | 
				
			||||
 | 
					    headers: { | 
				
			||||
 | 
					        // Disable client side cache, only server side cache.
 | 
				
			||||
 | 
					        // BUG! Not working for the second request
 | 
				
			||||
 | 
					        "cache-control": "no-cache", | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports = apicache; | 
				
			||||
@ -0,0 +1,59 @@ | 
				
			|||||
 | 
					function MemoryCache() { | 
				
			||||
 | 
					    this.cache = {}; | 
				
			||||
 | 
					    this.size = 0; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { | 
				
			||||
 | 
					    let old = this.cache[key]; | 
				
			||||
 | 
					    let instance = this; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let entry = { | 
				
			||||
 | 
					        value: value, | 
				
			||||
 | 
					        expire: time + Date.now(), | 
				
			||||
 | 
					        timeout: setTimeout(function () { | 
				
			||||
 | 
					            instance.delete(key); | 
				
			||||
 | 
					            return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key); | 
				
			||||
 | 
					        }, time) | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.cache[key] = entry; | 
				
			||||
 | 
					    this.size = Object.keys(this.cache).length; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    return entry; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					MemoryCache.prototype.delete = function (key) { | 
				
			||||
 | 
					    let entry = this.cache[key]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if (entry) { | 
				
			||||
 | 
					        clearTimeout(entry.timeout); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    delete this.cache[key]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.size = Object.keys(this.cache).length; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    return null; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					MemoryCache.prototype.get = function (key) { | 
				
			||||
 | 
					    let entry = this.cache[key]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    return entry; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					MemoryCache.prototype.getValue = function (key) { | 
				
			||||
 | 
					    let entry = this.get(key); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    return entry && entry.value; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					MemoryCache.prototype.clear = function () { | 
				
			||||
 | 
					    Object.keys(this.cache).forEach(function (key) { | 
				
			||||
 | 
					        this.delete(key); | 
				
			||||
 | 
					    }, this); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    return true; | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports = MemoryCache; | 
				
			||||
@ -0,0 +1,151 @@ | 
				
			|||||
 | 
					let express = require("express"); | 
				
			||||
 | 
					const { allowDevAllOrigin, getSettings, setting } = require("../util-server"); | 
				
			||||
 | 
					const { R } = require("redbean-node"); | 
				
			||||
 | 
					const server = require("../server"); | 
				
			||||
 | 
					const apicache = require("../modules/apicache"); | 
				
			||||
 | 
					const Monitor = require("../model/monitor"); | 
				
			||||
 | 
					let router = express.Router(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let cache = apicache.middleware; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					router.get("/api/entry-page", async (_, response) => { | 
				
			||||
 | 
					    allowDevAllOrigin(response); | 
				
			||||
 | 
					    response.json(server.entryPage); | 
				
			||||
 | 
					}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// Status Page Config
 | 
				
			||||
 | 
					router.get("/api/status-page/config", async (_request, response) => { | 
				
			||||
 | 
					    allowDevAllOrigin(response); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let config = await getSettings("statusPage"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if (! config.statusPageTheme) { | 
				
			||||
 | 
					        config.statusPageTheme = "light"; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if (! config.statusPagePublished) { | 
				
			||||
 | 
					        config.statusPagePublished = true; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if (! config.title) { | 
				
			||||
 | 
					        config.title = "Uptime Kuma"; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    response.json(config); | 
				
			||||
 | 
					}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// Status Page - Get the current Incident
 | 
				
			||||
 | 
					// Can fetch only if published
 | 
				
			||||
 | 
					router.get("/api/status-page/incident", async (_, response) => { | 
				
			||||
 | 
					    allowDevAllOrigin(response); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					        await checkPublished(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let incident = await R.findOne("incident", " pin = 1 AND active = 1"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (incident) { | 
				
			||||
 | 
					            incident = incident.toPublicJSON(); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        response.json({ | 
				
			||||
 | 
					            ok: true, | 
				
			||||
 | 
					            incident, | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					        send403(response, error.message); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// Status Page - Monitor List
 | 
				
			||||
 | 
					// Can fetch only if published
 | 
				
			||||
 | 
					router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => { | 
				
			||||
 | 
					    allowDevAllOrigin(response); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					        await checkPublished(); | 
				
			||||
 | 
					        const publicGroupList = []; | 
				
			||||
 | 
					        let list = await R.find("group", " public = 1 ORDER BY weight "); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        for (let groupBean of list) { | 
				
			||||
 | 
					            publicGroupList.push(await groupBean.toPublicJSON()); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        response.json(publicGroupList); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					        send403(response, error.message); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// Status Page Polling Data
 | 
				
			||||
 | 
					// Can fetch only if published
 | 
				
			||||
 | 
					router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => { | 
				
			||||
 | 
					    allowDevAllOrigin(response); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					        await checkPublished(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let heartbeatList = {}; | 
				
			||||
 | 
					        let uptimeList = {}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let monitorIDList = await R.getCol(` | 
				
			||||
 | 
					            SELECT monitor_group.monitor_id FROM monitor_group, \`group\` | 
				
			||||
 | 
					            WHERE monitor_group.group_id = \`group\`.id
 | 
				
			||||
 | 
					            AND public = 1 | 
				
			||||
 | 
					        `);
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        for (let monitorID of monitorIDList) { | 
				
			||||
 | 
					            let list = await R.getAll(` | 
				
			||||
 | 
					                    SELECT * FROM heartbeat | 
				
			||||
 | 
					                    WHERE monitor_id = ? | 
				
			||||
 | 
					                    ORDER BY time DESC | 
				
			||||
 | 
					                    LIMIT 50 | 
				
			||||
 | 
					            `, [
 | 
				
			||||
 | 
					                monitorID, | 
				
			||||
 | 
					            ]); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            list = R.convertToBeans("heartbeat", list); | 
				
			||||
 | 
					            heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON()); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            const type = 24; | 
				
			||||
 | 
					            uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        response.json({ | 
				
			||||
 | 
					            heartbeatList, | 
				
			||||
 | 
					            uptimeList | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					        send403(response, error.message); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					async function checkPublished() { | 
				
			||||
 | 
					    if (! await isPublished()) { | 
				
			||||
 | 
					        throw new Error("The status page is not published"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/** | 
				
			||||
 | 
					 * Default is published | 
				
			||||
 | 
					 * @returns {Promise<boolean>} | 
				
			||||
 | 
					 */ | 
				
			||||
 | 
					async function isPublished() { | 
				
			||||
 | 
					    const value = await setting("statusPagePublished"); | 
				
			||||
 | 
					    if (value === null) { | 
				
			||||
 | 
					        return true; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    return value; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					function send403(res, msg = "") { | 
				
			||||
 | 
					    res.status(403).json({ | 
				
			||||
 | 
					        "status": "fail", | 
				
			||||
 | 
					        "msg": msg, | 
				
			||||
 | 
					    }); | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports = router; | 
				
			||||
								
									
										File diff suppressed because it is too large
									
								
							
						
					@ -0,0 +1,161 @@ | 
				
			|||||
 | 
					const { R } = require("redbean-node"); | 
				
			||||
 | 
					const { checkLogin, setSettings } = require("../util-server"); | 
				
			||||
 | 
					const dayjs = require("dayjs"); | 
				
			||||
 | 
					const { debug } = require("../../src/util"); | 
				
			||||
 | 
					const ImageDataURI = require("../image-data-uri"); | 
				
			||||
 | 
					const Database = require("../database"); | 
				
			||||
 | 
					const apicache = require("../modules/apicache"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					module.exports.statusPageSocketHandler = (socket) => { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Post or edit incident
 | 
				
			||||
 | 
					    socket.on("postIncident", async (incident, callback) => { | 
				
			||||
 | 
					        try { | 
				
			||||
 | 
					            checkLogin(socket); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            await R.exec("UPDATE incident SET pin = 0 "); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            let incidentBean; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (incident.id) { | 
				
			||||
 | 
					                incidentBean = await R.findOne("incident", " id = ?", [ | 
				
			||||
 | 
					                    incident.id | 
				
			||||
 | 
					                ]); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (incidentBean == null) { | 
				
			||||
 | 
					                incidentBean = R.dispense("incident"); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            incidentBean.title = incident.title; | 
				
			||||
 | 
					            incidentBean.content = incident.content; | 
				
			||||
 | 
					            incidentBean.style = incident.style; | 
				
			||||
 | 
					            incidentBean.pin = true; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (incident.id) { | 
				
			||||
 | 
					                incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc()); | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                incidentBean.createdDate = R.isoDateTime(dayjs.utc()); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            await R.store(incidentBean); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            callback({ | 
				
			||||
 | 
					                ok: true, | 
				
			||||
 | 
					                incident: incidentBean.toPublicJSON(), | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } catch (error) { | 
				
			||||
 | 
					            callback({ | 
				
			||||
 | 
					                ok: false, | 
				
			||||
 | 
					                msg: error.message, | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    socket.on("unpinIncident", async (callback) => { | 
				
			||||
 | 
					        try { | 
				
			||||
 | 
					            checkLogin(socket); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            callback({ | 
				
			||||
 | 
					                ok: true, | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } catch (error) { | 
				
			||||
 | 
					            callback({ | 
				
			||||
 | 
					                ok: false, | 
				
			||||
 | 
					                msg: error.message, | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Save Status Page
 | 
				
			||||
 | 
					    // imgDataUrl Only Accept PNG!
 | 
				
			||||
 | 
					    socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        try { | 
				
			||||
 | 
					            checkLogin(socket); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            apicache.clear(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            const header = "data:image/png;base64,"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // Check logo format
 | 
				
			||||
 | 
					            // If is image data url, convert to png file
 | 
				
			||||
 | 
					            // Else assume it is a url, nothing to do
 | 
				
			||||
 | 
					            if (imgDataUrl.startsWith("data:")) { | 
				
			||||
 | 
					                if (! imgDataUrl.startsWith(header)) { | 
				
			||||
 | 
					                    throw new Error("Only allowed PNG logo."); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                // Convert to file
 | 
				
			||||
 | 
					                await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png"); | 
				
			||||
 | 
					                config.logo = "/upload/logo.png?t=" + Date.now(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                config.icon = imgDataUrl; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // Save Config
 | 
				
			||||
 | 
					            await setSettings("statusPage", config); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // Save Public Group List
 | 
				
			||||
 | 
					            const groupIDList = []; | 
				
			||||
 | 
					            let groupOrder = 1; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            for (let group of publicGroupList) { | 
				
			||||
 | 
					                let groupBean; | 
				
			||||
 | 
					                if (group.id) { | 
				
			||||
 | 
					                    groupBean = await R.findOne("group", " id = ? AND public = 1 ", [ | 
				
			||||
 | 
					                        group.id | 
				
			||||
 | 
					                    ]); | 
				
			||||
 | 
					                } else { | 
				
			||||
 | 
					                    groupBean = R.dispense("group"); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                groupBean.name = group.name; | 
				
			||||
 | 
					                groupBean.public = true; | 
				
			||||
 | 
					                groupBean.weight = groupOrder++; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                await R.store(groupBean); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [ | 
				
			||||
 | 
					                    groupBean.id | 
				
			||||
 | 
					                ]); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                let monitorOrder = 1; | 
				
			||||
 | 
					                console.log(group.monitorList); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                for (let monitor of group.monitorList) { | 
				
			||||
 | 
					                    let relationBean = R.dispense("monitor_group"); | 
				
			||||
 | 
					                    relationBean.weight = monitorOrder++; | 
				
			||||
 | 
					                    relationBean.group_id = groupBean.id; | 
				
			||||
 | 
					                    relationBean.monitor_id = monitor.id; | 
				
			||||
 | 
					                    await R.store(relationBean); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                groupIDList.push(groupBean.id); | 
				
			||||
 | 
					                group.id = groupBean.id; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // Delete groups that not in the list
 | 
				
			||||
 | 
					            debug("Delete groups that not in the list"); | 
				
			||||
 | 
					            const slots = groupIDList.map(() => "?").join(","); | 
				
			||||
 | 
					            await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            callback({ | 
				
			||||
 | 
					                ok: true, | 
				
			||||
 | 
					                publicGroupList, | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        } catch (error) { | 
				
			||||
 | 
					            console.log(error); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            callback({ | 
				
			||||
 | 
					                ok: false, | 
				
			||||
 | 
					                msg: error.message, | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					}; | 
				
			||||
@ -0,0 +1,144 @@ | 
				
			|||||
 | 
					<template> | 
				
			||||
 | 
					    <!-- Group List --> | 
				
			||||
 | 
					    <Draggable | 
				
			||||
 | 
					        v-model="$root.publicGroupList" | 
				
			||||
 | 
					        :disabled="!editMode" | 
				
			||||
 | 
					        item-key="id" | 
				
			||||
 | 
					        :animation="100" | 
				
			||||
 | 
					    > | 
				
			||||
 | 
					        <template #item="group"> | 
				
			||||
 | 
					            <div class="mb-5 "> | 
				
			||||
 | 
					                <!-- Group Title --> | 
				
			||||
 | 
					                <h2 class="group-title"> | 
				
			||||
 | 
					                    <font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" /> | 
				
			||||
 | 
					                    <font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" /> | 
				
			||||
 | 
					                    <Editable v-model="group.element.name" :contenteditable="editMode" tag="span" /> | 
				
			||||
 | 
					                </h2> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <div class="shadow-box monitor-list mt-4 position-relative"> | 
				
			||||
 | 
					                    <div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg"> | 
				
			||||
 | 
					                        {{ $t("No Monitors") }} | 
				
			||||
 | 
					                    </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                    <!-- Monitor List --> | 
				
			||||
 | 
					                    <!-- animation is not working, no idea why --> | 
				
			||||
 | 
					                    <Draggable | 
				
			||||
 | 
					                        v-model="group.element.monitorList" | 
				
			||||
 | 
					                        class="monitor-list" | 
				
			||||
 | 
					                        group="same-group" | 
				
			||||
 | 
					                        :disabled="!editMode" | 
				
			||||
 | 
					                        :animation="100" | 
				
			||||
 | 
					                        item-key="id" | 
				
			||||
 | 
					                    > | 
				
			||||
 | 
					                        <template #item="monitor"> | 
				
			||||
 | 
					                            <div class="item"> | 
				
			||||
 | 
					                                <div class="row"> | 
				
			||||
 | 
					                                    <div class="col-9 col-md-8 small-padding"> | 
				
			||||
 | 
					                                        <div class="info"> | 
				
			||||
 | 
					                                            <font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" /> | 
				
			||||
 | 
					                                            <font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" /> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                                            <Uptime :monitor="monitor.element" type="24" :pill="true" /> | 
				
			||||
 | 
					                                            {{ monitor.element.name }} | 
				
			||||
 | 
					                                        </div> | 
				
			||||
 | 
					                                    </div> | 
				
			||||
 | 
					                                    <div :key="$root.userHeartbeatBar" class="col-3 col-md-4"> | 
				
			||||
 | 
					                                        <HeartbeatBar size="small" :monitor-id="monitor.element.id" /> | 
				
			||||
 | 
					                                    </div> | 
				
			||||
 | 
					                                </div> | 
				
			||||
 | 
					                            </div> | 
				
			||||
 | 
					                        </template> | 
				
			||||
 | 
					                    </Draggable> | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					        </template> | 
				
			||||
 | 
					    </Draggable> | 
				
			||||
 | 
					</template> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					<script> | 
				
			||||
 | 
					import Draggable from "vuedraggable"; | 
				
			||||
 | 
					import HeartbeatBar from "./HeartbeatBar.vue"; | 
				
			||||
 | 
					import Uptime from "./Uptime.vue"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export default { | 
				
			||||
 | 
					    components: { | 
				
			||||
 | 
					        Draggable, | 
				
			||||
 | 
					        HeartbeatBar, | 
				
			||||
 | 
					        Uptime, | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    props: { | 
				
			||||
 | 
					        editMode: { | 
				
			||||
 | 
					            type: Boolean, | 
				
			||||
 | 
					            required: true, | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    data() { | 
				
			||||
 | 
					        return { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    computed: { | 
				
			||||
 | 
					        showGroupDrag() { | 
				
			||||
 | 
					            return (this.$root.publicGroupList.length >= 2); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    created() { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    methods: { | 
				
			||||
 | 
					        removeGroup(index) { | 
				
			||||
 | 
					            this.$root.publicGroupList.splice(index, 1); | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        removeMonitor(groupIndex, index) { | 
				
			||||
 | 
					            this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1); | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					</script> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					<style lang="scss" scoped> | 
				
			||||
 | 
					@import "../assets/vars"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.no-monitor-msg { | 
				
			||||
 | 
					    position: absolute; | 
				
			||||
 | 
					    width: 100%; | 
				
			||||
 | 
					    top: 20px; | 
				
			||||
 | 
					    left: 0; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.monitor-list { | 
				
			||||
 | 
					    min-height: 46px; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.flip-list-move { | 
				
			||||
 | 
					    transition: transform 0.5s; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.no-move { | 
				
			||||
 | 
					    transition: transform 0s; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.drag { | 
				
			||||
 | 
					    color: #bbb; | 
				
			||||
 | 
					    cursor: grab; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.remove { | 
				
			||||
 | 
					    color: $danger; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.group-title { | 
				
			||||
 | 
					    span { | 
				
			||||
 | 
					        display: inline-block; | 
				
			||||
 | 
					        min-width: 15px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.mobile { | 
				
			||||
 | 
					    .item { | 
				
			||||
 | 
					        padding: 13px 0 10px 0; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					</style> | 
				
			||||
@ -0,0 +1,182 @@ | 
				
			|||||
 | 
					export default { | 
				
			||||
 | 
					    languageName: "Português (Brasileiro)", | 
				
			||||
 | 
					    checkEverySecond: "Verificar cada {0} segundos.", | 
				
			||||
 | 
					    retryCheckEverySecond: "Tentar novamente a cada {0} segundos.", | 
				
			||||
 | 
					    retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada", | 
				
			||||
 | 
					    ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS", | 
				
			||||
 | 
					    upsideDownModeDescription: "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.", | 
				
			||||
 | 
					    maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.", | 
				
			||||
 | 
					    acceptedStatusCodesDescription: "Selecione os códigos de status que são considerados uma resposta bem-sucedida.", | 
				
			||||
 | 
					    passwordNotMatchMsg: "A senha repetida não corresponde.", | 
				
			||||
 | 
					    notificationDescription: "Atribua uma notificação ao (s) monitor (es) para que funcione.", | 
				
			||||
 | 
					    keywordDescription: "Pesquise a palavra-chave em html simples ou resposta JSON e diferencia maiúsculas de minúsculas", | 
				
			||||
 | 
					    pauseDashboardHome: "Pausar", | 
				
			||||
 | 
					    deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?", | 
				
			||||
 | 
					    deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?", | 
				
			||||
 | 
					    resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.", | 
				
			||||
 | 
					    rrtypeDescription: "Selecione o RR-Type que você deseja monitorar", | 
				
			||||
 | 
					    pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?", | 
				
			||||
 | 
					    enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.", | 
				
			||||
 | 
					    clearEventsMsg: "Tem certeza de que deseja excluir todos os eventos deste monitor?", | 
				
			||||
 | 
					    clearHeartbeatsMsg: "Tem certeza de que deseja excluir todos os heartbeats deste monitor?", | 
				
			||||
 | 
					    confirmClearStatisticsMsg: "Tem certeza que deseja excluir TODAS as estatísticas?", | 
				
			||||
 | 
					    importHandleDescription: "Escolha 'Ignorar existente' se quiser ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.", | 
				
			||||
 | 
					    confirmImportMsg: "Tem certeza que deseja importar o backup? Certifique-se de que selecionou a opção de importação correta.", | 
				
			||||
 | 
					    twoFAVerifyLabel: "Digite seu token para verificar se 2FA está funcionando", | 
				
			||||
 | 
					    tokenValidSettingsMsg: "O token é válido! Agora você pode salvar as configurações 2FA.", | 
				
			||||
 | 
					    confirmEnableTwoFAMsg: "Tem certeza de que deseja habilitar 2FA?", | 
				
			||||
 | 
					    confirmDisableTwoFAMsg: "Tem certeza de que deseja desativar 2FA?", | 
				
			||||
 | 
					    Settings: "Configurações", | 
				
			||||
 | 
					    Dashboard: "Dashboard", | 
				
			||||
 | 
					    "New Update": "Nova Atualização", | 
				
			||||
 | 
					    Language: "Linguagem", | 
				
			||||
 | 
					    Appearance: "Aparência", | 
				
			||||
 | 
					    Theme: "Tema", | 
				
			||||
 | 
					    General: "Geral", | 
				
			||||
 | 
					    Version: "Versão", | 
				
			||||
 | 
					    "Check Update On GitHub": "Verificar atualização no Github", | 
				
			||||
 | 
					    List: "Lista", | 
				
			||||
 | 
					    Add: "Adicionar", | 
				
			||||
 | 
					    "Add New Monitor": "Adicionar novo monitor", | 
				
			||||
 | 
					    "Quick Stats": "Estatísticas rápidas", | 
				
			||||
 | 
					    Up: "On", | 
				
			||||
 | 
					    Down: "Off", | 
				
			||||
 | 
					    Pending: "Pendente", | 
				
			||||
 | 
					    Unknown: "Desconhecido", | 
				
			||||
 | 
					    Pause: "Pausar", | 
				
			||||
 | 
					    Name: "Nome", | 
				
			||||
 | 
					    Status: "Status", | 
				
			||||
 | 
					    DateTime: "Data hora", | 
				
			||||
 | 
					    Message: "Mensagem", | 
				
			||||
 | 
					    "No important events": "Nenhum evento importante", | 
				
			||||
 | 
					    Resume: "Resumo", | 
				
			||||
 | 
					    Edit: "Editar", | 
				
			||||
 | 
					    Delete: "Deletar", | 
				
			||||
 | 
					    Current: "Atual", | 
				
			||||
 | 
					    Uptime: "Tempo de atividade", | 
				
			||||
 | 
					    "Cert Exp.": "Cert Exp.", | 
				
			||||
 | 
					    days: "dias", | 
				
			||||
 | 
					    day: "dia", | 
				
			||||
 | 
					    "-day": "-dia", | 
				
			||||
 | 
					    hour: "hora", | 
				
			||||
 | 
					    "-hour": "-hora", | 
				
			||||
 | 
					    Response: "Resposta", | 
				
			||||
 | 
					    Ping: "Ping", | 
				
			||||
 | 
					    "Monitor Type": "Tipo de Monitor", | 
				
			||||
 | 
					    Keyword: "Palavra-Chave", | 
				
			||||
 | 
					    "Friendly Name": "Nome Amigável", | 
				
			||||
 | 
					    URL: "URL", | 
				
			||||
 | 
					    Hostname: "Hostname", | 
				
			||||
 | 
					    Port: "Porta", | 
				
			||||
 | 
					    "Heartbeat Interval": "Intervalo de Heartbeat", | 
				
			||||
 | 
					    Retries: "Novas tentativas", | 
				
			||||
 | 
					    "Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat", | 
				
			||||
 | 
					    Advanced: "Avançado", | 
				
			||||
 | 
					    "Upside Down Mode": "Modo de cabeça para baixo", | 
				
			||||
 | 
					    "Max. Redirects": "Redirecionamento Máx.", | 
				
			||||
 | 
					    "Accepted Status Codes": "Status Code Aceitáveis", | 
				
			||||
 | 
					    Save: "Salvar", | 
				
			||||
 | 
					    Notifications: "Notificações", | 
				
			||||
 | 
					    "Not available, please setup.": "Não disponível, por favor configure.", | 
				
			||||
 | 
					    "Setup Notification": "Configurar Notificação", | 
				
			||||
 | 
					    Light: "Claro", | 
				
			||||
 | 
					    Dark: "Escuro", | 
				
			||||
 | 
					    Auto: "Auto", | 
				
			||||
 | 
					    "Theme - Heartbeat Bar": "Tema - Barra de Heartbeat", | 
				
			||||
 | 
					    Normal: "Normal", | 
				
			||||
 | 
					    Bottom: "Inferior", | 
				
			||||
 | 
					    None: "Nenhum", | 
				
			||||
 | 
					    Timezone: "Fuso horário", | 
				
			||||
 | 
					    "Search Engine Visibility": "Visibilidade do mecanismo de pesquisa", | 
				
			||||
 | 
					    "Allow indexing": "Permitir Indexação", | 
				
			||||
 | 
					    "Discourage search engines from indexing site": "Desencoraje os motores de busca de indexar o site", | 
				
			||||
 | 
					    "Change Password": "Mudar senha", | 
				
			||||
 | 
					    "Current Password": "Senha atual", | 
				
			||||
 | 
					    "New Password": "Nova Senha", | 
				
			||||
 | 
					    "Repeat New Password": "Repetir Nova Senha", | 
				
			||||
 | 
					    "Update Password": "Atualizar Senha", | 
				
			||||
 | 
					    "Disable Auth": "Desativar Autenticação", | 
				
			||||
 | 
					    "Enable Auth": "Ativar Autenticação", | 
				
			||||
 | 
					    Logout: "Deslogar", | 
				
			||||
 | 
					    Leave: "Sair", | 
				
			||||
 | 
					    "I understand, please disable": "Eu entendo, por favor desative.", | 
				
			||||
 | 
					    Confirm: "Confirmar", | 
				
			||||
 | 
					    Yes: "Sim", | 
				
			||||
 | 
					    No: "Não", | 
				
			||||
 | 
					    Username: "Usuário", | 
				
			||||
 | 
					    Password: "Senha", | 
				
			||||
 | 
					    "Remember me": "Lembre-me", | 
				
			||||
 | 
					    Login: "Autenticar", | 
				
			||||
 | 
					    "No Monitors, please": "Nenhum monitor, por favor", | 
				
			||||
 | 
					    "add one": "adicionar um", | 
				
			||||
 | 
					    "Notification Type": "Tipo de Notificação", | 
				
			||||
 | 
					    Email: "Email", | 
				
			||||
 | 
					    Test: "Testar", | 
				
			||||
 | 
					    "Certificate Info": "Info. do Certificado ", | 
				
			||||
 | 
					    "Resolver Server": "Resolver Servidor", | 
				
			||||
 | 
					    "Resource Record Type": "Tipo de registro de aplicação", | 
				
			||||
 | 
					    "Last Result": "Último resultado", | 
				
			||||
 | 
					    "Create your admin account": "Crie sua conta de admin", | 
				
			||||
 | 
					    "Repeat Password": "Repita a senha", | 
				
			||||
 | 
					    "Import Backup": "Importar Backup", | 
				
			||||
 | 
					    "Export Backup": "Exportar Backup", | 
				
			||||
 | 
					    Export: "Exportar", | 
				
			||||
 | 
					    Import: "Importar", | 
				
			||||
 | 
					    respTime: "Tempo de Resp. (ms)", | 
				
			||||
 | 
					    notAvailableShort: "N/A", | 
				
			||||
 | 
					    "Default enabled": "Padrão habilitado", | 
				
			||||
 | 
					    "Apply on all existing monitors": "Aplicar em todos os monitores existentes", | 
				
			||||
 | 
					    Create: "Criar", | 
				
			||||
 | 
					    "Clear Data": "Limpar Dados", | 
				
			||||
 | 
					    Events: "Eventos", | 
				
			||||
 | 
					    Heartbeats: "Heartbeats", | 
				
			||||
 | 
					    "Auto Get": "Obter Automático", | 
				
			||||
 | 
					    backupDescription: "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.", | 
				
			||||
 | 
					    backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.", | 
				
			||||
 | 
					    backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantenha-o com cuidado.", | 
				
			||||
 | 
					    alertNoFile: "Selecione um arquivo para importar.", | 
				
			||||
 | 
					    alertWrongFileType: "Selecione um arquivo JSON.", | 
				
			||||
 | 
					    "Clear all statistics": "Limpar todas as estatísticas", | 
				
			||||
 | 
					    "Skip existing": "Pular existente", | 
				
			||||
 | 
					    Overwrite: "Sobrescrever", | 
				
			||||
 | 
					    Options: "Opções", | 
				
			||||
 | 
					    "Keep both": "Manter os dois", | 
				
			||||
 | 
					    "Verify Token": "Verificar Token", | 
				
			||||
 | 
					    "Setup 2FA": "Configurar 2FA", | 
				
			||||
 | 
					    "Enable 2FA": "Ativar 2FA", | 
				
			||||
 | 
					    "Disable 2FA": "Desativar 2FA", | 
				
			||||
 | 
					    "2FA Settings": "Configurações do 2FA ", | 
				
			||||
 | 
					    "Two Factor Authentication": "Autenticação e Dois Fatores", | 
				
			||||
 | 
					    Active: "Ativo", | 
				
			||||
 | 
					    Inactive: "Inativo", | 
				
			||||
 | 
					    Token: "Token", | 
				
			||||
 | 
					    "Show URI": "Mostrar URI", | 
				
			||||
 | 
					    Tags: "Tag", | 
				
			||||
 | 
					    "Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...", | 
				
			||||
 | 
					    "Tag with this name already exist.": "Já existe uma etiqueta com este nome.", | 
				
			||||
 | 
					    "Tag with this value already exist.": "Já existe uma etiqueta com este valor.", | 
				
			||||
 | 
					    color: "cor", | 
				
			||||
 | 
					    "value (optional)": "valor (opcional)", | 
				
			||||
 | 
					    Gray: "Cinza", | 
				
			||||
 | 
					    Red: "Vermelho", | 
				
			||||
 | 
					    Orange: "Laranja", | 
				
			||||
 | 
					    Green: "Verde", | 
				
			||||
 | 
					    Blue: "Azul", | 
				
			||||
 | 
					    Indigo: "Índigo", | 
				
			||||
 | 
					    Purple: "Roxo", | 
				
			||||
 | 
					    Pink: "Rosa", | 
				
			||||
 | 
					    "Search...": "Buscar...", | 
				
			||||
 | 
					    "Avg. Ping": "Ping Médio.", | 
				
			||||
 | 
					    "Avg. Response": "Resposta Média. ", | 
				
			||||
 | 
					    "Status Page": "Página de Status", | 
				
			||||
 | 
					    "Entry Page": "Página de entrada", | 
				
			||||
 | 
					    "statusPageNothing": "Nada aqui, por favor, adicione um grupo ou monitor.", | 
				
			||||
 | 
					    "No Services": "Nenhum Serviço", | 
				
			||||
 | 
					    "All Systems Operational": "Todos os Serviços Operacionais", | 
				
			||||
 | 
					    "Partially Degraded Service": "Serviço parcialmente degradado", | 
				
			||||
 | 
					    "Degraded Service": "Serviço Degradado", | 
				
			||||
 | 
					    "Add Group": "Adicionar Grupo", | 
				
			||||
 | 
					    "Add a monitor": "Adicionar um monitor", | 
				
			||||
 | 
					    "Edit Status Page": "Editar Página de Status", | 
				
			||||
 | 
					    "Go to Dashboard": "Ir para a dashboard", | 
				
			||||
 | 
					}; | 
				
			||||
@ -0,0 +1,40 @@ | 
				
			|||||
 | 
					import axios from "axios"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const env = process.env.NODE_ENV || "production"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// change the axios base url for development
 | 
				
			||||
 | 
					if (env === "development" || localStorage.dev === "dev") { | 
				
			||||
 | 
					    axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001"; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export default { | 
				
			||||
 | 
					    data() { | 
				
			||||
 | 
					        return { | 
				
			||||
 | 
					            publicGroupList: [], | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    computed: { | 
				
			||||
 | 
					        publicMonitorList() { | 
				
			||||
 | 
					            let result = {}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            for (let group of this.publicGroupList) { | 
				
			||||
 | 
					                for (let monitor of group.monitorList) { | 
				
			||||
 | 
					                    result[monitor.id] = monitor; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            return result; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        publicLastHeartbeatList() { | 
				
			||||
 | 
					            let result = {}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            for (let monitorID in this.publicMonitorList) { | 
				
			||||
 | 
					                if (this.lastHeartbeatList[monitorID]) { | 
				
			||||
 | 
					                    result[monitorID] = this.lastHeartbeatList[monitorID]; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            return result; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					}; | 
				
			||||
@ -0,0 +1,20 @@ | 
				
			|||||
 | 
					<template> | 
				
			||||
 | 
					    <div></div> | 
				
			||||
 | 
					</template> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					<script> | 
				
			||||
 | 
					import axios from "axios"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export default { | 
				
			||||
 | 
					    async mounted() { | 
				
			||||
 | 
					        let entryPage = (await axios.get("/api/entry-page")).data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (entryPage === "statusPage") { | 
				
			||||
 | 
					            this.$router.push("/status"); | 
				
			||||
 | 
					        } else { | 
				
			||||
 | 
					            this.$router.push("/dashboard"); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					</script> | 
				
			||||
@ -0,0 +1,653 @@ | 
				
			|||||
 | 
					<template> | 
				
			||||
 | 
					    <div v-if="loadedTheme" class="container mt-3"> | 
				
			||||
 | 
					        <!-- Logo & Title --> | 
				
			||||
 | 
					        <h1 class="mb-4"> | 
				
			||||
 | 
					            <!-- Logo --> | 
				
			||||
 | 
					            <span class="logo-wrapper" @click="showImageCropUploadMethod"> | 
				
			||||
 | 
					                <img :src="logoURL" alt class="logo me-2" :class="logoClass" /> | 
				
			||||
 | 
					                <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> | 
				
			||||
 | 
					            </span> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <!-- Uploader --> | 
				
			||||
 | 
					            <!--    url="/api/status-page/upload-logo" --> | 
				
			||||
 | 
					            <ImageCropUpload v-model="showImageCropUpload" | 
				
			||||
 | 
					                             field="img" | 
				
			||||
 | 
					                             :width="128" | 
				
			||||
 | 
					                             :height="128" | 
				
			||||
 | 
					                             :langType="$i18n.locale" | 
				
			||||
 | 
					                             img-format="png" | 
				
			||||
 | 
					                             :noCircle="true" | 
				
			||||
 | 
					                             :noSquare="false" | 
				
			||||
 | 
					                             @crop-success="cropSuccess" | 
				
			||||
 | 
					            /> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <!-- Title --> | 
				
			||||
 | 
					            <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" /> | 
				
			||||
 | 
					        </h1> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        <!-- Admin functions --> | 
				
			||||
 | 
					        <div v-if="hasToken" class="mb-4"> | 
				
			||||
 | 
					            <div v-if="!enableEditMode"> | 
				
			||||
 | 
					                <button class="btn btn-info me-2" @click="edit"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="edit" /> | 
				
			||||
 | 
					                    {{ $t("Edit Status Page") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <a href="/dashboard" class="btn btn-info"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="tachometer-alt" /> | 
				
			||||
 | 
					                    {{ $t("Go to Dashboard") }} | 
				
			||||
 | 
					                </a> | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <div v-else> | 
				
			||||
 | 
					                <button class="btn btn-success me-2" @click="save"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="save" /> | 
				
			||||
 | 
					                    {{ $t("Save") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <button class="btn btn-danger me-2" @click="discard"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="save" /> | 
				
			||||
 | 
					                    {{ $t("Discard") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <button class="btn btn-primary btn-add-group me-2" @click="createIncident"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="bullhorn" /> | 
				
			||||
 | 
					                    {{ $t("Create Incident") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <!-- | 
				
			||||
 | 
					                <button v-if="isPublished" class="btn btn-light me-2" @click=""> | 
				
			||||
 | 
					                    <font-awesome-icon icon="save" /> | 
				
			||||
 | 
					                    {{ $t("Unpublish") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <button v-if="!isPublished" class="btn btn-info me-2" @click=""> | 
				
			||||
 | 
					                    <font-awesome-icon icon="save" /> | 
				
			||||
 | 
					                    {{ $t("Publish") }} | 
				
			||||
 | 
					                </button>--> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <!-- Set Default Language --> | 
				
			||||
 | 
					                <!-- Set theme --> | 
				
			||||
 | 
					                <button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="save" /> | 
				
			||||
 | 
					                    {{ $t("Switch to Light Theme") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="save" /> | 
				
			||||
 | 
					                    {{ $t("Switch to Dark Theme") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        <!-- Incident --> | 
				
			||||
 | 
					        <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass"> | 
				
			||||
 | 
					            <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong> | 
				
			||||
 | 
					            <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" /> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong> | 
				
			||||
 | 
					            <Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" /> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <!-- Incident Date --> | 
				
			||||
 | 
					            <div class="date mt-3"> | 
				
			||||
 | 
					                Created: {{ incident.createdDate }} ({{ createdDateFromNow }})<br /> | 
				
			||||
 | 
					                <span v-if="incident.lastUpdatedDate"> | 
				
			||||
 | 
					                    Last Updated: {{ incident.lastUpdatedDate }} ({{ lastUpdatedDateFromNow }}) | 
				
			||||
 | 
					                </span> | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <div v-if="editMode" class="mt-3"> | 
				
			||||
 | 
					                <button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="bullhorn" /> | 
				
			||||
 | 
					                    {{ $t("Post") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="edit" /> | 
				
			||||
 | 
					                    {{ $t("Edit") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="times" /> | 
				
			||||
 | 
					                    {{ $t("Cancel") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <div v-if="editIncidentMode" class="dropdown d-inline-block me-2"> | 
				
			||||
 | 
					                    <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> | 
				
			||||
 | 
					                        Style: {{ incident.style }} | 
				
			||||
 | 
					                    </button> | 
				
			||||
 | 
					                    <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1"> | 
				
			||||
 | 
					                        <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">info</a></li> | 
				
			||||
 | 
					                        <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">warning</a></li> | 
				
			||||
 | 
					                        <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">danger</a></li> | 
				
			||||
 | 
					                        <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">primary</a></li> | 
				
			||||
 | 
					                        <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">light</a></li> | 
				
			||||
 | 
					                        <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">dark</a></li> | 
				
			||||
 | 
					                    </ul> | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="unlink" /> | 
				
			||||
 | 
					                    {{ $t("Unpin") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        <!-- Overall Status --> | 
				
			||||
 | 
					        <div class="shadow-box list  p-4 overall-status mb-4"> | 
				
			||||
 | 
					            <div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData"> | 
				
			||||
 | 
					                <font-awesome-icon icon="question-circle" class="ok" /> | 
				
			||||
 | 
					                {{ $t("No Services") }} | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <template v-else> | 
				
			||||
 | 
					                <div v-if="allUp"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="check-circle" class="ok" /> | 
				
			||||
 | 
					                    {{ $t("All Systems Operational") }} | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <div v-else-if="partialDown"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="exclamation-circle" class="warning" /> | 
				
			||||
 | 
					                    {{ $t("Partially Degraded Service") }} | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <div v-else-if="allDown"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="times-circle" class="danger" /> | 
				
			||||
 | 
					                    {{ $t("Degraded Service") }} | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                <div v-else> | 
				
			||||
 | 
					                    <font-awesome-icon icon="question-circle" style="color: #efefef" /> | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					            </template> | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        <!-- Description --> | 
				
			||||
 | 
					        <strong v-if="editMode">{{ $t("Description") }}:</strong> | 
				
			||||
 | 
					        <Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" /> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        <div v-if="editMode" class="mb-4"> | 
				
			||||
 | 
					            <div> | 
				
			||||
 | 
					                <button class="btn btn-primary btn-add-group me-2" @click="addGroup"> | 
				
			||||
 | 
					                    <font-awesome-icon icon="plus" /> | 
				
			||||
 | 
					                    {{ $t("Add Group") }} | 
				
			||||
 | 
					                </button> | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <div class="mt-3"> | 
				
			||||
 | 
					                <div v-if="allMonitorList.length > 0 && loadedData"> | 
				
			||||
 | 
					                    <label>{{ $t("Add a monitor") }}:</label> | 
				
			||||
 | 
					                    <select v-model="selectedMonitor" class="form-control"> | 
				
			||||
 | 
					                        <option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option> | 
				
			||||
 | 
					                    </select> | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					                <div v-else class="text-center"> | 
				
			||||
 | 
					                    {{ $t("No monitors available.") }}  <router-link to="/add">{{ $t("Add one") }}</router-link> | 
				
			||||
 | 
					                </div> | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        <div class="mb-4"> | 
				
			||||
 | 
					            <div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center"> | 
				
			||||
 | 
					                <!-- 👀 Nothing here, please add a group or a monitor. --> | 
				
			||||
 | 
					                👀 {{ $t("statusPageNothing") }} | 
				
			||||
 | 
					            </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            <PublicGroupList :edit-mode="enableEditMode" /> | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        <footer class="mt-5 mb-4"> | 
				
			||||
 | 
					            Powered by <a target="_blank" href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a> | 
				
			||||
 | 
					        </footer> | 
				
			||||
 | 
					    </div> | 
				
			||||
 | 
					</template> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					<script> | 
				
			||||
 | 
					import axios from "axios"; | 
				
			||||
 | 
					import PublicGroupList from "../components/PublicGroupList.vue"; | 
				
			||||
 | 
					import ImageCropUpload from "vue-image-crop-upload"; | 
				
			||||
 | 
					import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts"; | 
				
			||||
 | 
					import { useToast } from "vue-toastification"; | 
				
			||||
 | 
					import dayjs from "dayjs"; | 
				
			||||
 | 
					const toast = useToast(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const leavePageMsg = "Do you really want to leave? you have unsaved changes!"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					let feedInterval; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export default { | 
				
			||||
 | 
					    components: { | 
				
			||||
 | 
					        PublicGroupList, | 
				
			||||
 | 
					        ImageCropUpload | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Leave Page for vue route change | 
				
			||||
 | 
					    beforeRouteLeave(to, from, next) { | 
				
			||||
 | 
					        if (this.editMode) { | 
				
			||||
 | 
					            const answer = window.confirm(leavePageMsg); | 
				
			||||
 | 
					            if (answer) { | 
				
			||||
 | 
					                next(); | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                next(false); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        next(); | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    data() { | 
				
			||||
 | 
					        return { | 
				
			||||
 | 
					            enableEditMode: false, | 
				
			||||
 | 
					            enableEditIncidentMode: false, | 
				
			||||
 | 
					            hasToken: false, | 
				
			||||
 | 
					            config: {}, | 
				
			||||
 | 
					            selectedMonitor: null, | 
				
			||||
 | 
					            incident: null, | 
				
			||||
 | 
					            previousIncident: null, | 
				
			||||
 | 
					            showImageCropUpload: false, | 
				
			||||
 | 
					            imgDataUrl: "/icon.svg", | 
				
			||||
 | 
					            loadedTheme: false, | 
				
			||||
 | 
					            loadedData: false, | 
				
			||||
 | 
					            baseURL: "", | 
				
			||||
 | 
					        }; | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    computed: { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        logoURL() { | 
				
			||||
 | 
					            if (this.imgDataUrl.startsWith("data:")) { | 
				
			||||
 | 
					                return this.imgDataUrl; | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                return this.baseURL + this.imgDataUrl; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        /** | 
				
			||||
 | 
					         * If the monitor is added to public list, which will not be in this list. | 
				
			||||
 | 
					         */ | 
				
			||||
 | 
					        allMonitorList() { | 
				
			||||
 | 
					            let result = []; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            for (let id in this.$root.monitorList) { | 
				
			||||
 | 
					                if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) { | 
				
			||||
 | 
					                    let monitor = this.$root.monitorList[id]; | 
				
			||||
 | 
					                    result.push(monitor); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            return result; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        editMode() { | 
				
			||||
 | 
					            return this.enableEditMode && this.$root.socket.connected; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        editIncidentMode() { | 
				
			||||
 | 
					            return this.enableEditIncidentMode; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        isPublished() { | 
				
			||||
 | 
					            return this.config.statusPagePublished; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        theme() { | 
				
			||||
 | 
					            return this.config.statusPageTheme; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        logoClass() { | 
				
			||||
 | 
					            if (this.editMode) { | 
				
			||||
 | 
					                return { | 
				
			||||
 | 
					                    "edit-mode": true, | 
				
			||||
 | 
					                }; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            return {}; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        incidentClass() { | 
				
			||||
 | 
					            return "bg-" + this.incident.style; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        overallStatus() { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) { | 
				
			||||
 | 
					                return -1; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            let status = STATUS_PAGE_ALL_UP; | 
				
			||||
 | 
					            let hasUp = false; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            for (let id in this.$root.publicLastHeartbeatList) { | 
				
			||||
 | 
					                let beat = this.$root.publicLastHeartbeatList[id]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                if (beat.status === UP) { | 
				
			||||
 | 
					                    hasUp = true; | 
				
			||||
 | 
					                } else { | 
				
			||||
 | 
					                    status = STATUS_PAGE_PARTIAL_DOWN; | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (! hasUp) { | 
				
			||||
 | 
					                status = STATUS_PAGE_ALL_DOWN; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            return status; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        allUp() { | 
				
			||||
 | 
					            return this.overallStatus === STATUS_PAGE_ALL_UP; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        partialDown() { | 
				
			||||
 | 
					            return this.overallStatus === STATUS_PAGE_PARTIAL_DOWN; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        allDown() { | 
				
			||||
 | 
					            return this.overallStatus === STATUS_PAGE_ALL_DOWN; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        createdDateFromNow() { | 
				
			||||
 | 
					            return dayjs.utc(this.incident.createdDate).fromNow(); | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        lastUpdatedDateFromNow() { | 
				
			||||
 | 
					            return dayjs.utc(this.incident. lastUpdatedDate).fromNow(); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    watch: { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        /** | 
				
			||||
 | 
					         * Selected a monitor and add to the list. | 
				
			||||
 | 
					         */ | 
				
			||||
 | 
					        selectedMonitor(monitor) { | 
				
			||||
 | 
					            if (monitor) { | 
				
			||||
 | 
					                if (this.$root.publicGroupList.length === 0) { | 
				
			||||
 | 
					                    this.addGroup(); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                const firstGroup = this.$root.publicGroupList[0]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                firstGroup.monitorList.push(monitor); | 
				
			||||
 | 
					                this.selectedMonitor = null; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // Set Theme | 
				
			||||
 | 
					        "config.statusPageTheme"() { | 
				
			||||
 | 
					            this.$root.statusPageTheme = this.config.statusPageTheme; | 
				
			||||
 | 
					            this.loadedTheme = true; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        "config.title"(title) { | 
				
			||||
 | 
					            document.title = title; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    async created() { | 
				
			||||
 | 
					        this.hasToken = ("token" in this.$root.storage()); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // Browser change page | 
				
			||||
 | 
					        // https://stackoverflow.com/questions/7317273/warn-user-before-leaving-web-page-with-unsaved-changes | 
				
			||||
 | 
					        window.addEventListener("beforeunload", (e) => { | 
				
			||||
 | 
					            if (this.editMode) { | 
				
			||||
 | 
					                (e || window.event).returnValue = leavePageMsg; | 
				
			||||
 | 
					                return leavePageMsg; | 
				
			||||
 | 
					            } else { | 
				
			||||
 | 
					                return null; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // Special handle for dev | 
				
			||||
 | 
					        const env = process.env.NODE_ENV; | 
				
			||||
 | 
					        if (env === "development" || localStorage.dev === "dev") { | 
				
			||||
 | 
					            this.baseURL = location.protocol + "//" + location.hostname + ":3001"; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    async mounted() { | 
				
			||||
 | 
					        axios.get("/api/status-page/config").then((res) => { | 
				
			||||
 | 
					            this.config = res.data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (this.config.logo) { | 
				
			||||
 | 
					                this.imgDataUrl = this.config.logo; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        axios.get("/api/status-page/incident").then((res) => { | 
				
			||||
 | 
					            if (res.data.ok) { | 
				
			||||
 | 
					                this.incident = res.data.incident; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        axios.get("/api/status-page/monitor-list").then((res) => { | 
				
			||||
 | 
					            this.$root.publicGroupList = res.data; | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // 5mins a loop | 
				
			||||
 | 
					        this.updateHeartbeatList(); | 
				
			||||
 | 
					        feedInterval = setInterval(() => { | 
				
			||||
 | 
					            this.updateHeartbeatList(); | 
				
			||||
 | 
					        }, (300 + 10) * 1000); | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    methods: { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        updateHeartbeatList() { | 
				
			||||
 | 
					            // If editMode, it will use the data from websocket. | 
				
			||||
 | 
					            if (! this.editMode) { | 
				
			||||
 | 
					                axios.get("/api/status-page/heartbeat").then((res) => { | 
				
			||||
 | 
					                    this.$root.heartbeatList = res.data.heartbeatList; | 
				
			||||
 | 
					                    this.$root.uptimeList = res.data.uptimeList; | 
				
			||||
 | 
					                    this.loadedData = true; | 
				
			||||
 | 
					                }); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        edit() { | 
				
			||||
 | 
					            this.$root.initSocketIO(true); | 
				
			||||
 | 
					            this.enableEditMode = true; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        save() { | 
				
			||||
 | 
					            this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => { | 
				
			||||
 | 
					                if (res.ok) { | 
				
			||||
 | 
					                    this.enableEditMode = false; | 
				
			||||
 | 
					                    this.$root.publicGroupList = res.publicGroupList; | 
				
			||||
 | 
					                    location.reload(); | 
				
			||||
 | 
					                } else { | 
				
			||||
 | 
					                    toast.error(res.msg); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        monitorSelectorLabel(monitor) { | 
				
			||||
 | 
					            return `${monitor.name}`; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        addGroup() { | 
				
			||||
 | 
					            let groupName = "Untitled Group"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (this.$root.publicGroupList.length === 0) { | 
				
			||||
 | 
					                groupName = "Services"; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            this.$root.publicGroupList.push({ | 
				
			||||
 | 
					                name: groupName, | 
				
			||||
 | 
					                monitorList: [], | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        discard() { | 
				
			||||
 | 
					            location.reload(); | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        changeTheme(name) { | 
				
			||||
 | 
					            this.config.statusPageTheme = name; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        /** | 
				
			||||
 | 
					         * Crop Success | 
				
			||||
 | 
					         */ | 
				
			||||
 | 
					        cropSuccess(imgDataUrl) { | 
				
			||||
 | 
					            this.imgDataUrl = imgDataUrl; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        showImageCropUploadMethod() { | 
				
			||||
 | 
					            if (this.editMode) { | 
				
			||||
 | 
					                this.showImageCropUpload = true; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        createIncident() { | 
				
			||||
 | 
					            this.enableEditIncidentMode = true; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (this.incident) { | 
				
			||||
 | 
					                this.previousIncident = this.incident; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            this.incident = { | 
				
			||||
 | 
					                title: "", | 
				
			||||
 | 
					                content: "", | 
				
			||||
 | 
					                style: "primary", | 
				
			||||
 | 
					            }; | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        postIncident() { | 
				
			||||
 | 
					            if (this.incident.title == "" || this.incident.content == "") { | 
				
			||||
 | 
					                toast.error("Please input title and content."); | 
				
			||||
 | 
					                return; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            this.$root.getSocket().emit("postIncident", this.incident, (res) => { | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                if (res.ok) { | 
				
			||||
 | 
					                    this.enableEditIncidentMode = false; | 
				
			||||
 | 
					                    this.incident = res.incident; | 
				
			||||
 | 
					                } else { | 
				
			||||
 | 
					                    toast.error(res.msg); | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        /** | 
				
			||||
 | 
					         * Click Edit Button | 
				
			||||
 | 
					         */ | 
				
			||||
 | 
					        editIncident() { | 
				
			||||
 | 
					            this.enableEditIncidentMode = true; | 
				
			||||
 | 
					            this.previousIncident = Object.assign({}, this.incident); | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        cancelIncident() { | 
				
			||||
 | 
					            this.enableEditIncidentMode = false; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if (this.previousIncident) { | 
				
			||||
 | 
					                this.incident = this.previousIncident; | 
				
			||||
 | 
					                this.previousIncident = null; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        unpinIncident() { | 
				
			||||
 | 
					            this.$root.getSocket().emit("unpinIncident", () => { | 
				
			||||
 | 
					                this.incident = null; | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					</script> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					<style lang="scss" scoped> | 
				
			||||
 | 
					@import "../assets/vars.scss"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.overall-status { | 
				
			||||
 | 
					    font-weight: bold; | 
				
			||||
 | 
					    font-size: 25px; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    .ok { | 
				
			||||
 | 
					        color: $primary; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    .warning { | 
				
			||||
 | 
					        color: $warning; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    .danger { | 
				
			||||
 | 
					        color: $danger; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					h1 { | 
				
			||||
 | 
					    font-size: 30px; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    img { | 
				
			||||
 | 
					        vertical-align: middle; | 
				
			||||
 | 
					        height: 60px; | 
				
			||||
 | 
					        width: 60px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					footer { | 
				
			||||
 | 
					    text-align: center; | 
				
			||||
 | 
					    font-size: 14px; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.description span { | 
				
			||||
 | 
					    min-width: 50px; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.logo-wrapper { | 
				
			||||
 | 
					    display: inline-block; | 
				
			||||
 | 
					    position: relative; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    &:hover { | 
				
			||||
 | 
					        .icon-upload { | 
				
			||||
 | 
					            transform: scale(1.2); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    .icon-upload { | 
				
			||||
 | 
					        transition: all $easing-in 0.2s; | 
				
			||||
 | 
					        position: absolute; | 
				
			||||
 | 
					        bottom: 6px; | 
				
			||||
 | 
					        font-size: 20px; | 
				
			||||
 | 
					        left: -14px; | 
				
			||||
 | 
					        background-color: white; | 
				
			||||
 | 
					        padding: 5px; | 
				
			||||
 | 
					        border-radius: 10px; | 
				
			||||
 | 
					        cursor: pointer; | 
				
			||||
 | 
					        box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.logo { | 
				
			||||
 | 
					    transition: all $easing-in 0.2s; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    &.edit-mode { | 
				
			||||
 | 
					        cursor: pointer; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        &:hover { | 
				
			||||
 | 
					            transform: scale(1.2); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.incident { | 
				
			||||
 | 
					    .content { | 
				
			||||
 | 
					        &[contenteditable=true] { | 
				
			||||
 | 
					            min-height: 60px; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    .date { | 
				
			||||
 | 
					        font-size: 12px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.mobile { | 
				
			||||
 | 
					    h1 { | 
				
			||||
 | 
					        font-size: 22px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    .overall-status { | 
				
			||||
 | 
					        font-size: 20px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					</style> | 
				
			||||
					Loading…
					
					
				
		Reference in new issue