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 @@
+
+
+
+
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 {
-
+
+
`
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)
}