From c9fa183712cd611eb455dfa4afca15dfe19c7f23 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sun, 10 Apr 2022 00:25:27 +0800 Subject: [PATCH] Manage domain names --- server/model/status_page.js | 45 ++++++++++++- .../status-page-socket-handler.js | 35 ++++++++-- src/assets/app.scss | 16 +++++ src/icon.js | 2 + src/pages/Entry.vue | 4 +- src/pages/StatusPage.vue | 67 ++++++++++++++++++- 6 files changed, 157 insertions(+), 12 deletions(-) diff --git a/server/model/status_page.js b/server/model/status_page.js index 7f03fe6d..1383d3b0 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -10,7 +10,7 @@ class StatusPage extends BeanModel { * @returns {Promise} */ static async loadDomainMappingList() { - this.domainMappingList = await R.getAssoc(` + StatusPage.domainMappingList = await R.getAssoc(` SELECT domain, slug FROM status_page, status_page_cname WHERE status_page.id = status_page_cname.status_page_id @@ -30,7 +30,46 @@ class StatusPage extends BeanModel { return list; } - getDomainList() { + 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]; @@ -52,7 +91,7 @@ class StatusPage extends BeanModel { theme: this.theme, published: !!this.published, showTags: !!this.show_tags, - domainList: this.getDomainList(), + domainNameList: this.getDomainNameList(), }; } diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 55a70d71..c844136e 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 9e37cc99..0b27c6a6 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 c03db755..7201b94f 100644 --- a/src/icon.js +++ b/src/icon.js @@ -38,6 +38,7 @@ import { faExternalLinkSquareAlt, faSpinner, faUndo, + faPlusCircle, } from "@fortawesome/free-solid-svg-icons"; library.add( @@ -75,6 +76,7 @@ library.add( faExternalLinkSquareAlt, faSpinner, faUndo, + faPlusCircle, ); export { FontAwesomeIcon }; diff --git a/src/pages/Entry.vue b/src/pages/Entry.vue index fd24a352..40aeb0b2 100644 --- a/src/pages/Entry.vue +++ b/src/pages/Entry.vue @@ -1,6 +1,6 @@ @@ -27,7 +27,7 @@ export default { if (res.type === "statusPageMatchedDomain") { this.statusPageSlug = res.statusPageSlug; - } else if (res.type === "entryPage") { // Dev only + } else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side const entryPage = res.entryPage; if (entryPage === "statusPage") { diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 74e258bc..2c0c1967 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -36,9 +36,19 @@ +
- - + + +
    +
  • + + +
  • +
@@ -305,7 +315,6 @@ export default { loadedData: false, baseURL: "", clickedEditButton: false, - domainNamesPlaceholder: "example1.com\nexample2.com\n..." }; }, computed: { @@ -400,6 +409,22 @@ export default { }, watch: { + /** + * If connected to the socket and logged in, request private data of this statusPage + * @param connected + */ + "$root.loggedIn"(loggedIn) { + if (loggedIn) { + this.$root.getSocket().emit("getStatusPage", this.slug, (res) => { + if (res.ok) { + this.config = res.config; + } else { + toast.error(res.msg); + } + }); + } + }, + /** * Selected a monitor and add to the list. */ @@ -469,6 +494,10 @@ export default { axios.get("/api/status-page/" + this.slug).then((res) => { this.config = res.data.config; + if (!this.config.domainNameList) { + this.config.domainNameList = []; + } + if (this.config.icon) { this.imgDataUrl = this.config.icon; } @@ -586,6 +615,10 @@ export default { }); }, + addDomainField() { + this.config.domainNameList.push(""); + }, + discard() { location.href = "/status/" + this.slug; }, @@ -668,6 +701,10 @@ export default { return dayjs.utc(date).fromNow(); }, + removeDomain(index) { + this.config.domainNameList.splice(index, 1); + }, + } }; @@ -733,6 +770,7 @@ h1 { .sidebar-footer { border-top: 1px solid #ededed; + border-right: 1px solid #ededed; padding: 10px; width: 300px; height: 70px; @@ -740,6 +778,8 @@ h1 { left: 0; bottom: 0; background-color: white; + display: flex; + align-items: center; } } @@ -826,10 +866,31 @@ 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; + } + } + } +} +