diff --git a/server/database.js b/server/database.js index 69b0fd3c0..b852cc985 100644 --- a/server/database.js +++ b/server/database.js @@ -129,6 +129,11 @@ class Database { await R.exec("PRAGMA cache_size = -12000"); await R.exec("PRAGMA auto_vacuum = FULL"); + // This ensures that an operating system crash or power failure will not corrupt the database. + // FULL synchronous is very safe, but it is also slower. + // Read more: https://sqlite.org/pragma.html#pragma_synchronous + await R.exec("PRAGMA synchronous = FULL"); + if (!noLog) { console.log("SQLite config:"); console.log(await R.getAll("PRAGMA journal_mode")); diff --git a/server/model/status_page.js b/server/model/status_page.js index 6f763f586..1383d3b00 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -3,6 +3,20 @@ const { R } = require("redbean-node"); class StatusPage extends BeanModel { + static domainMappingList = { }; + + /** + * Return object like this: { "test-uptime.kuma.pet": "default" } + * @returns {Promise} + */ + static async loadDomainMappingList() { + StatusPage.domainMappingList = await R.getAssoc(` + SELECT domain, slug + FROM status_page, status_page_cname + WHERE status_page.id = status_page_cname.status_page_id + `); + } + static async sendStatusPageList(io, socket) { let result = {}; @@ -16,6 +30,57 @@ class StatusPage extends BeanModel { return list; } + async updateDomainNameList(domainNameList) { + + if (!Array.isArray(domainNameList)) { + throw new Error("Invalid array"); + } + + let trx = await R.begin(); + + await trx.exec("DELETE FROM status_page_cname WHERE status_page_id = ?", [ + this.id, + ]); + + try { + for (let domain of domainNameList) { + if (typeof domain !== "string") { + throw new Error("Invalid domain"); + } + + if (domain.trim() === "") { + continue; + } + + // If the domain name is used in another status page, delete it + await trx.exec("DELETE FROM status_page_cname WHERE domain = ?", [ + domain, + ]); + + let mapping = trx.dispense("status_page_cname"); + mapping.status_page_id = this.id; + mapping.domain = domain; + await trx.store(mapping); + } + await trx.commit(); + } catch (error) { + await trx.rollback(); + throw error; + } + } + + getDomainNameList() { + let domainList = []; + for (let domain in StatusPage.domainMappingList) { + let s = StatusPage.domainMappingList[domain]; + + if (this.slug === s) { + domainList.push(domain); + } + } + return domainList; + } + async toJSON() { return { id: this.id, @@ -26,6 +91,7 @@ class StatusPage extends BeanModel { theme: this.theme, published: !!this.published, showTags: !!this.show_tags, + domainNameList: this.getDomainNameList(), }; } diff --git a/server/routers/api-router.js b/server/routers/api-router.js index ad8870847..6f463b6b0 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -12,9 +12,19 @@ let router = express.Router(); let cache = apicache.middleware; let io = server.io; -router.get("/api/entry-page", async (_, response) => { +router.get("/api/entry-page", async (request, response) => { allowDevAllOrigin(response); - response.json(server.entryPage); + + let result = { }; + + if (request.hostname in StatusPage.domainMappingList) { + result.type = "statusPageMatchedDomain"; + result.statusPageSlug = StatusPage.domainMappingList[request.hostname]; + } else { + result.type = "entryPage"; + result.entryPage = server.entryPage; + } + response.json(result); }); router.get("/api/push/:pushToken", async (request, response) => { diff --git a/server/server.js b/server/server.js index 8b732d9b5..01941ab48 100644 --- a/server/server.js +++ b/server/server.js @@ -211,6 +211,7 @@ try { await initDatabase(testMode); exports.entryPage = await setting("entryPage"); + await StatusPage.loadDomainMappingList(); console.log("Adding route"); @@ -219,8 +220,13 @@ try { // *************************** // Entry Page - app.get("/", async (_request, response) => { - if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) { + app.get("/", async (request, response) => { + debug(`Request Domain: ${request.hostname}`); + + if (request.hostname in StatusPage.domainMappingList) { + debug("This is a status page domain"); + response.send(indexHTML); + } else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) { response.redirect("/status/" + exports.entryPage.replace("statusPage-", "")); } else { response.redirect("/dashboard"); diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 55a70d711..c844136ea 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -85,15 +85,35 @@ module.exports.statusPageSocketHandler = (socket) => { } }); + socket.on("getStatusPage", async (slug, callback) => { + try { + checkLogin(socket); + + let statusPage = await R.findOne("status_page", " slug = ? ", [ + slug + ]); + + if (!statusPage) { + throw new Error("No slug?"); + } + + callback({ + ok: true, + config: await statusPage.toJSON(), + }); + } catch (error) { + callback({ + ok: false, + msg: error.message, + }); + } + }); + // Save Status Page // imgDataUrl Only Accept PNG! socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => { - try { - checkSlug(config.slug); - checkLogin(socket); - apicache.clear(); // Save Config let statusPage = await R.findOne("status_page", " slug = ? ", [ @@ -104,6 +124,8 @@ module.exports.statusPageSocketHandler = (socket) => { throw new Error("No slug?"); } + checkSlug(config.slug); + const header = "data:image/png;base64,"; // Check logo format @@ -137,6 +159,9 @@ module.exports.statusPageSocketHandler = (socket) => { await R.store(statusPage); + await statusPage.updateDomainNameList(config.domainNameList); + await StatusPage.loadDomainMappingList(); + // Save Public Group List const groupIDList = []; let groupOrder = 1; @@ -193,6 +218,8 @@ module.exports.statusPageSocketHandler = (socket) => { await setSetting("entryPage", server.entryPage, "general"); } + apicache.clear(); + callback({ ok: true, publicGroupList, diff --git a/src/assets/app.scss b/src/assets/app.scss index 9e37cc99b..0b27c6a6e 100644 --- a/src/assets/app.scss +++ b/src/assets/app.scss @@ -22,6 +22,18 @@ textarea.form-control { width: 10px; } +.list-group { + border-radius: 0.75rem; + + .dark & { + .list-group-item { + background-color: $dark-bg; + color: $dark-font-color; + border-color: $dark-border-color; + } + } +} + ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 20px; @@ -412,6 +424,10 @@ textarea.form-control { background-color: rgba(239, 239, 239, 0.7); border-radius: 8px; + &.no-bg { + background-color: transparent !important; + } + &:focus { outline: 0 solid #eee; background-color: rgba(245, 245, 245, 0.9); diff --git a/src/icon.js b/src/icon.js index bbd816ea0..7201b94fb 100644 --- a/src/icon.js +++ b/src/icon.js @@ -37,6 +37,8 @@ import { faPen, faExternalLinkSquareAlt, faSpinner, + faUndo, + faPlusCircle, } from "@fortawesome/free-solid-svg-icons"; library.add( @@ -73,6 +75,8 @@ library.add( faPen, faExternalLinkSquareAlt, faSpinner, + faUndo, + faPlusCircle, ); export { FontAwesomeIcon }; diff --git a/src/languages/zh-HK.js b/src/languages/zh-HK.js index 561e5de94..0c282f372 100644 --- a/src/languages/zh-HK.js +++ b/src/languages/zh-HK.js @@ -339,7 +339,7 @@ export default { "Switch to Dark Theme": "切換至深色佈景主題", "Show Tags": "顯示標籤", "Hide Tags": "隱藏標籤", - Description: "說明", + Description: "描述", "No monitors available.": "沒有可用的監測器。", "Add one": "新增一個", "No Monitors": "無監測器", @@ -347,7 +347,6 @@ export default { Services: "服務", Discard: "捨棄", Cancel: "取消", - "Powered by": "技術支援", shrinkDatabaseDescription: "觸發 SQLite 的資料庫清理 (VACUUM)。如果您的資料庫是在 1.10.0 版本後建立,AUTO_VACUUM 已自動啟用,則無需此操作。", serwersms: "SerwerSMS.pl", serwersmsAPIUser: "API 使用者名稱 (包括 webapi_ 前綴)", diff --git a/src/pages/Entry.vue b/src/pages/Entry.vue index 6148ec56d..40aeb0b2b 100644 --- a/src/pages/Entry.vue +++ b/src/pages/Entry.vue @@ -1,19 +1,44 @@ @@ -705,9 +753,7 @@ h1 { top: 0; width: 300px; height: 100vh; - padding: 15px 15px 68px 15px; - overflow-x: hidden; - overflow-y: auto; + border-right: 1px solid #ededed; .danger-zone { @@ -715,13 +761,25 @@ h1 { padding-top: 15px; } + .sidebar-body { + padding: 0 10px 10px 10px; + overflow-x: hidden; + overflow-y: auto; + height: calc(100% - 70px); + } + .sidebar-footer { - width: 100%; - bottom: 0; - left: 0; - padding: 15px; - position: absolute; border-top: 1px solid #ededed; + border-right: 1px solid #ededed; + padding: 10px; + width: 300px; + height: 70px; + position: fixed; + left: 0; + bottom: 0; + background-color: white; + display: flex; + align-items: center; } } @@ -808,7 +866,29 @@ footer { } .sidebar-footer { + border-right-color: $dark-border-color; border-top-color: $dark-border-color; + background-color: $dark-header-bg; + } + } +} + +.domain-name-list { + li { + display: flex; + align-items: center; + padding: 10px 0 10px 10px; + + .domain-input { + flex-grow: 1; + background-color: transparent; + border: none; + color: $dark-font-color; + outline: none; + + &::placeholder { + color: #1d2634; + } } } }