diff --git a/routeros_device.go b/routeros_device.go index 4c65e25..50c19d1 100644 --- a/routeros_device.go +++ b/routeros_device.go @@ -58,7 +58,7 @@ func (dev *RouterosDevice) Init() { // {{{ // query sends a RouterOS REST API query and returns the unparsed body. func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{ - url := fmt.Sprintf("https://%s/rest%s", dev.Host, path) + url := fmt.Sprintf("https://%s:%d/rest%s", dev.Host, dev.Port, path) logger.Info("URL", "method", method, "url", url) var request *http.Request @@ -150,6 +150,19 @@ func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (entry DNSEntry, err err = json.Unmarshal(body, &entry) return }// }}} +func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) { + _, err = dev.query("DELETE", "/ip/dns/static/"+id, []byte{}) + if err != nil { + rosError := struct{ Detail string }{} + if jsonError := json.Unmarshal([]byte(err.Error()), &rosError); jsonError == nil { + logger.Error("routeros", "error", jsonError) + err = errors.New(rosError.Detail) + return + } + return + } + return +} /* // FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router diff --git a/static/css/index.css b/static/css/index.css index 7d2f395..e54174e 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -20,7 +20,7 @@ --record-NXDOMAIN: #aa0000; --record-other: #888; - --record-hover: #fffff8; + --record-hover: #fffff4; } html { @@ -164,9 +164,8 @@ button { margin-left: 10px; display: grid; - grid-template-columns: repeat(5, min-content); + grid-template-columns: repeat(6, min-content); width: min-content; - /*grid-gap: 4px 10px;*/ align-items: center; border-left: 1px solid var(--line-color); @@ -174,7 +173,7 @@ button { font-weight: bold; font-size: 0.75em; background: #eee; - padding: 2px 8px; + padding: 4px 8px; border-left: 1px solid var(--header-line); border-top: 1px solid var(--header-line); border-bottom: 1px solid var(--header-line); @@ -319,6 +318,18 @@ button { .ttl { cursor: pointer; + border-left: 1px solid var(--header-line); + border-bottom: 1px solid var(--header-line); + padding: 0px 8px; + height: 100%; + align-content: center; + + &.mouse-over { + background-color: var(--record-hover); + } + } + + .actions { border-left: 1px solid var(--header-line); border-right: 1px solid var(--header-line); border-bottom: 1px solid var(--header-line); @@ -329,6 +340,11 @@ button { &.mouse-over { background-color: var(--record-hover); } + + img { + display: block; + cursor: pointer; + } } } diff --git a/static/images/icon_delete.svg b/static/images/icon_delete.svg new file mode 100644 index 0000000..0bc82d4 --- /dev/null +++ b/static/images/icon_delete.svg @@ -0,0 +1,49 @@ + + + + + + + + trash-can-outline + + + diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 6297f25..3148375 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -29,6 +29,10 @@ export class Application { this.cleanFolders(folder) }) }// }}} + deleteRecord(id) {// {{{ + const i = this.records.findIndex(rec => rec.id() == id) + this.records.splice(i, 1) + }// }}} renderFolders() {// {{{ this.records.sort(this.sortRecords) @@ -236,7 +240,8 @@ class Folder {
FQDN
Type
Value
-
TTL
+
TTL
+
Actions
` this.divSubfolders = this.div.querySelector('.subfolders') @@ -259,6 +264,17 @@ class Folder { else this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'block') + // Remove old ones + console.log(`removing records from ${this.name()}`) + for (const recdiv of this.divRecords.children) { + if (recdiv?.classList?.contains('fqdn')) { + const rec = this.records.find(r => r.id() == recdiv.dataset.record_id) + if (!rec) + this.divRecords.querySelectorAll(`[data-record_id="${recdiv.dataset.record_id}"]`) + .forEach(el => el.remove()) + } + } + for (const rec of Array.from(this.records)) this.divRecords.append(...rec.render()) @@ -285,6 +301,7 @@ class Record { this.divType = null this.divValue = null this.divTTL = null + this.divActions = null }// }}} id() {// {{{ @@ -347,24 +364,24 @@ class Record { this.divType = document.createElement('div') this.divValue = document.createElement('div') this.divTTL = document.createElement('div') + this.divActions = document.createElement('div') this.imgIcon.innerHTML = `` - this.imgIcon.classList.add("record-icon") + this.imgIcon.classList.add("record-icon"); + + [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions] + .forEach(div => { + div.addEventListener('mouseenter', () => this.mouseEnter()) + div.addEventListener('mouseleave', () => this.mouseLeave()) + div.dataset.record_id = this.id() + }) + this.divType.classList.add(this.type()) this.divFQDN.classList.add('fqdn') this.divType.classList.add('type') - this.divType.classList.add(this.type()) this.divValue.classList.add('value') this.divTTL.classList.add('ttl') - - this.divFQDN.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divFQDN.addEventListener('mouseleave', ()=>this.mouseLeave()) - this.divType.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divType.addEventListener('mouseleave', ()=>this.mouseLeave()) - this.divValue.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divValue.addEventListener('mouseleave', ()=>this.mouseLeave()) - this.divTTL.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divTTL.addEventListener('mouseleave', ()=>this.mouseLeave()) + this.divActions.classList.add('actions') this.divType.innerHTML = `
` this.divFQDN.innerHTML = ` @@ -372,6 +389,9 @@ class Record { ` + this.divActions.innerHTML = ` + + ` this.divFQDN.addEventListener('click', event => { if (event.shiftKey) @@ -387,6 +407,7 @@ class Record { }) this.divType.addEventListener('click', () => this.edit()) this.divTTL.addEventListener('click', () => this.edit()) + this.divActions.querySelector('.delete').addEventListener('click', () => this.delete()) } // FQDN is updated. @@ -404,20 +425,22 @@ class Record { this.divValue.innerText = this.value() this.divTTL.innerText = this.ttl() - return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL] + return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions] }// }}} - mouseEnter() { + mouseEnter() {// {{{ this.divFQDN.classList.add('mouse-over') this.divType.classList.add('mouse-over') this.divValue.classList.add('mouse-over') this.divTTL.classList.add('mouse-over') - } - mouseLeave() { + this.divActions.classList.add('mouse-over') + }// }}} + mouseLeave() {// {{{ this.divFQDN.classList.remove('mouse-over') this.divType.classList.remove('mouse-over') this.divValue.classList.remove('mouse-over') this.divTTL.classList.remove('mouse-over') - } + this.divActions.classList.remove('mouse-over') + }// }}} save() {// {{{ const created = (this.id() == '') @@ -456,6 +479,23 @@ class Record { } }) }// }}} + delete() {// {{{ + if (!confirm(`Are you sure you want to delete ${this.name()}?`)) + return + + fetch(`/record/delete/${this.id()}`) + .then(data => data.json()) + .then(json => { + if (!json.OK) { + alert(json.Error) + return + } + _app.deleteRecord(this.id()) + _app.cleanFolders() + _app.renderFolders() + _app.render() + }) + }// }}} openParentFolders(folder) {// {{{ if (folder === undefined) diff --git a/webserver.go b/webserver.go index 56f75c8..fc916ec 100644 --- a/webserver.go +++ b/webserver.go @@ -7,6 +7,7 @@ import ( // Standard "embed" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -33,6 +34,7 @@ func registerWebserverHandlers() { http.HandleFunc("/", rootHandler) http.HandleFunc("/record/save", actionRecordSave) + http.HandleFunc("/record/delete/{id}", actionRecordDelete) } func startWebserver() { @@ -110,6 +112,30 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) { record = NewDNSRecord(entry) - j, _ := json.Marshal(struct{ OK bool; Record DNSRecord }{true, record}) + j, _ := json.Marshal(struct { + OK bool + Record DNSRecord + }{true, record}) + w.Write(j) +} + +func actionRecordDelete(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + + if id == "" { + httpError(w, errors.New("No ID provided")) + return + } + + err := device.DeleteDNSEntry(id) + if err != nil { + httpError(w, err) + logger.Error("webserver", "op", "record_delete", "error", err) + return + } + + j, _ := json.Marshal(struct { + OK bool + }{true}) w.Write(j) }