diff --git a/routeros_device.go b/routeros_device.go index 50c19d1..4c65e25 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:%d/rest%s", dev.Host, dev.Port, path) + url := fmt.Sprintf("https://%s/rest%s", dev.Host, path) logger.Info("URL", "method", method, "url", url) var request *http.Request @@ -150,19 +150,6 @@ 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 ba6c2a1..6957ab1 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -2,6 +2,9 @@ --line-color: #ccc; --line-color-record: #eee; + --type-background: #ddd; + --type-foreground: #000; + --label-first: #800033; --label-rest: #666; @@ -9,18 +12,6 @@ --label-border: #ccc; --copy-color: #d48700; - - --header-line: #d0d0d0; - - --record-A: #89a02c; - --record-AAAA: #2f4858; - --record-CNAME: #f6ae2d; - --record-FWD: #f26419; - --record-TXT: #86bbd8; - --record-NXDOMAIN: #aa0000; - --record-other: #888; - - --record-hover: #fffff4; } html { @@ -106,10 +97,6 @@ button { cursor: pointer; } -#search { - margin-bottom: 16px; -} - #records-tree { white-space: nowrap; @@ -120,10 +107,6 @@ button { padding-left: 0px; } - &.no-domain > .label { - font-style: italic; - } - &.open { &>.label>img.open { display: block; @@ -146,7 +129,7 @@ button { &>.label { display: grid; - grid-template-columns: repeat(4, min-content); + grid-template-columns: min-content min-content 1fr; align-items: center; padding: 5px 0px; cursor: pointer; @@ -157,18 +140,6 @@ button { margin-right: 6px; display: none; } - - img.create { - display: none; - height: 16px; - margin-left: 8px; - } - - &:hover { - img.create { - display: block; - } - } } &>.subfolders { @@ -178,51 +149,17 @@ button { &>.records { padding-left: 30px; - padding-top: 8px; - padding-bottom: 8px; - margin-left: 10px; display: grid; - grid-template-columns: repeat(6, min-content); - width: min-content; + grid-template-columns: repeat(3, min-content) 1fr; + grid-gap: 4px 10px; align-items: center; border-left: 1px solid var(--line-color); - .header { - font-weight: bold; - font-size: 0.75em; - background: #eee; - padding: 4px 8px; - border-left: 1px solid var(--header-line); - border-top: 1px solid var(--header-line); - border-bottom: 1px solid var(--header-line); - - &:first-child { - grid-column: 1 / 3; - border-top-left-radius: 4px; - } - - &.last { - border-right: 1px solid var(--header-line); - border-top-right-radius: 4px; - } - } - - &>:not(.header) { - height: 40px; - } - - &>.record-icon { - border-left: 1px solid var(--header-line); - align-content: center; - padding-left: 8px; - border-bottom: 1px solid var(--header-line); - - img { - display: block; - padding-left: 4px; - } + &>img { + display: block; + padding-left: 4px; } .copy { @@ -237,14 +174,6 @@ button { cursor: pointer; user-select: none; display: flex; - padding: 0 16px 0 8px; - border-bottom: 1px solid var(--header-line); - height: 100%; - align-items: center; - - &.mouse-over { - background-color: var(--record-hover); - } &.created { * { @@ -269,107 +198,34 @@ button { .rest-label { color: var(--label-rest); - - &.no-domain { - font-style: italic; - } } } .type { - padding: 2px 8px; - border-bottom: 1px solid var(--header-line); - border-left: 1px solid var(--header-line); - align-content: center; - cursor: pointer; - - &.mouse-over { - background-color: var(--record-hover); - } - - div { - background-color: var(--record-other); - color: #fff; - - padding: 4px 8px; - border-radius: 4px; - width: min-content; - height: min-content; - font-weight: bold; - font-size: 0.85em; - } - - &.A div { - background-color: var(--record-A); - } - - &.AAAA div { - background-color: var(--record-AAAA); - } - - &.CNAME div { - background-color: var(--record-CNAME); - color: #000; - } - - &.FWD div { - background-color: var(--record-FWD); - } - - &.TXT div { - background-color: var(--record-TXT); - color: #000; - } - - &.NXDOMAIN div { - background-color: var(--record-NXDOMAIN); - } + background-color: var(--type-background); + color: var(--type-foreground); + padding: 4px 8px; + border-radius: 4px; + margin-top: 2px; + margin-bottom: 2px; + width: min-content; + font-weight: bold; + font-size: 0.85em; } .value { cursor: pointer; user-select: none; - 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); + .separator { + grid-column: 1 / -1; + + &:not(:last-child) { + border-bottom: 1px solid var(--line-color-record); } } - .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); - padding: 0px 8px; - height: 100%; - align-content: center; - - &.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 deleted file mode 100644 index 0bc82d4..0000000 --- a/static/images/icon_delete.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - trash-can-outline - - - diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 8f9195d..431521c 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -8,22 +8,10 @@ export class Application { this.topFolder = new Folder(this, null, 'root') this.recordsTree = null this.settingsIcon = null - this.createIcon = null - - this.searchFor = '' this.renderFolders() this.render() }// }}} - filteredRecords() {// {{{ - if (this.searchFor === '') - return this.records - - const records = this.records.filter(r => { - return (r.name().includes(this.searchFor)) - }) - return records - }// }}} parseRecords(recordsData) {// {{{ const records = recordsData.map(d => new Record(d)) return records @@ -41,24 +29,14 @@ export class Application { this.cleanFolders(folder) }) }// }}} - deleteRecord(id) {// {{{ - const i = this.records.findIndex(rec => rec.id() == id) - this.records.splice(i, 1) - }// }}} renderFolders() {// {{{ - const records = this.filteredRecords() - records.sort(this.sortRecords) + this.records.sort(this.sortRecords) - for (const rec of records) { - // It felt wrong when records for the base domain (e.g. google.com) was put in the top level domain (.com). - // While technically correct, the first label (com) is grouped together with the second label (google). - // Labels are counted reversely since the top domain is most significant. - // - // The `if` here is the exception for records that only has the two first labels. It would otherwise just - // be an empty array of labels and thus discarded. - let labels = rec.labels().reverse().slice(0, -1) - if (rec.labels().length == 1) - labels = rec.labels() + + // rec: for example www.google.com + for (const rec of this.records) { + // com.google (reverse and remove wwww) + const labels = rec.labels().reverse().slice(0, -1) // Start each record from the top and iterate through all its labels // except the first one since that would be the actual record. @@ -115,7 +93,10 @@ export class Application { return 0 }// }}} render() {// {{{ - if (this.createIcon === null) { + if (this.recordsTree == null) { + this.recordsTree = document.createElement('div') + this.recordsTree.id = 'records-tree' + this.createIcon = document.createElement('img') this.createIcon.id = 'create-icon' this.createIcon.src = `/images/${_VERSION}/icon_create.svg` @@ -126,36 +107,13 @@ export class Application { this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg` this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show()) - const searchEl = document.createElement('div') - searchEl.id = 'search' - searchEl.innerHTML = ` - - - ` - this.searchField = searchEl.querySelector('.search-for') - this.searchField.addEventListener('keydown', event => { - if (event.key == 'Enter') - this.search() - }) - const search = searchEl.querySelector('button.search') - search.addEventListener('click', () => this.search()) - + document.body.appendChild(this.recordsTree) document.body.appendChild(this.settingsIcon) document.body.appendChild(this.createIcon) - document.body.appendChild(searchEl) document.body.addEventListener('keydown', event => this.handlerKeys(event)) } - // The recordstree is deleted when making a search and is therefore - // not created along with icons and search above. - if (this.recordsTree === null) { - this.recordsTree = document.createElement('div') - this.recordsTree.id = 'records-tree' - - document.body.appendChild(this.recordsTree) - } - // Top root folder doesn't have to be shown. const folders = Array.from(this.topFolder.subfolders.values()) folders.sort(this.sortFolders) @@ -163,31 +121,11 @@ export class Application { for (const folder of folders) this.recordsTree.append(folder.render()) - this.removeEmptyFolders() - // Subscribe to settings update since the elements they will change // exists now. _mbus.subscribe('settings_updated', event => this.handlerSettingsUpdated(event.detail)) this.setBoxedFolders(this.settings.get('boxed_folders')) - }// }}} - removeEmptyFolders(folder) {// {{{ - if (folder === undefined) - folder = this.topFolder - folder.subfolders.forEach((folder, _label) => { - this.removeEmptyFolders(folder) - }) - - // This is a leaf folder in the tree. - // It has to be removed from the parent as well, since that could be up for - // removal as well, all the way up the chain. - if (folder.subfolders.size === 0 && folder.records.length === 0) { - if (folder.parentFolder) { - folder.parentFolder.subfolders.delete(folder.labels()[0]) - } - - folder.div?.remove() - } }// }}} handlerKeys(event) {// {{{ let handled = true @@ -205,10 +143,6 @@ export class Application { new RecordDialog(new Record()).show() break - case 'f': - this.searchField.focus() - break - default: handled = false } @@ -229,16 +163,6 @@ export class Application { else document.body.classList.remove('boxed-folders') }// }}} - search() {// {{{ - this.searchFor = this.searchField.value.trim().toLowerCase() - this.recordsTree.remove() - this.topFolder = new Folder(this, null, 'root') - this.recordsTree = null - - this.cleanFolders() - this.renderFolders() - this.render() - }// }}} } class Folder { @@ -260,18 +184,7 @@ class Folder { return this.folderName.toLowerCase() }// }}} labels() {// {{{ - let labels = this.name().split('.') - - // It is very uncommon to see just the top level domain. - // We're much more used to google.com than com and then google. - // First level is therefore the two most significant labels concatenated. - if (labels.length > 1) { - labels.reverse() - labels = [`${labels[1]}.${labels[0]}`].concat(labels.slice(2)) - labels.reverse() - } - - return labels + return this.name().split('.') }// }}} addRecord(rec) {// {{{ this.records.push(rec) @@ -317,27 +230,14 @@ class Folder { ${firstLabel}${restLabels != '' ? '.' + restLabels : ''} -
-
-
FQDN
-
Type
-
Value
-
TTL
-
Actions
-
+
` this.divSubfolders = this.div.querySelector('.subfolders') this.divRecords = this.div.querySelector('.records') this.div.querySelector('.label').addEventListener('click', event => this.toggleFolder(event)) - this.div.querySelector('.label .create').addEventListener('click', event => this.createRecord(event)) - - if (this.name() == '_no.domain') { - console.log('wut') - this.div.classList.add('no-domain') - } } @@ -349,26 +249,11 @@ class Folder { this.divSubfolders.append(folder.render()) // Records are refreshed. - if (this.records.length == 0) - this.divRecords.style.display = 'none' - else - this.divRecords.style.display = '' - - // Remove old ones - 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()) // Open this folder automatically if it is a toplevel folder and the settings change to open. - _mbus.subscribe('settings_updated', ({ detail: { key, value } }) => { + _mbus.subscribe('settings_updated', ({ detail: { key, value }}) => { if (key !== 'toplevel_open') return @@ -378,11 +263,6 @@ class Folder { return this.div }// }}} - createRecord(event) {// {{{ - event.stopPropagation() - const record = new Record({ Name: this.name() }) - new RecordDialog(record).show() - }// }}} } class Record { @@ -394,8 +274,7 @@ class Record { this.divFQDN = null this.divType = null this.divValue = null - this.divTTL = null - this.divActions = null + this.divSeparator = null }// }}} id() {// {{{ @@ -423,24 +302,7 @@ class Record { return this.data.MatchSubdomain === 'true' }// }}} labels() {// {{{ - let labels = this.name().split('.') - - if (labels.length === 1) { - labels = [labels[0], '_no', 'domain'] - } - - // It is very uncommon to see just the top level domain. - // We're much more used to google.com than com and then google. - // First level is therefore the two most significant labels concatenated. - if (labels.length > 1) { - labels.reverse() - labels = [`${labels[1]}.${labels[0]}`].concat(labels.slice(2)) - labels.reverse() - } else { - console.log(this, labels) - } - - return labels + return this.name().split('.') }// }}} copy(el, text) {// {{{ @@ -456,8 +318,6 @@ class Record { if (value.slice(0, 2) == '*.') { this.data['Name'] = value.slice(2) this.data['MatchSubdomain'] = 'true' - } else if (value.slice(-11) == '._no.domain') { - this.data['Name'] = value.slice(0, -11) } else { this.data['Name'] = value this.data['MatchSubdomain'] = 'false' @@ -472,39 +332,23 @@ class Record { }// }}} render() {// {{{ if (this.divFQDN === null) { - this.imgIcon = document.createElement('div') + this.imgIcon = document.createElement('img') this.divFQDN = document.createElement('div') 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, 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.divSeparator = document.createElement('div') + this.imgIcon.src = `/images/${_VERSION}/icon_record.svg` this.divFQDN.classList.add('fqdn') this.divType.classList.add('type') this.divValue.classList.add('value') - this.divTTL.classList.add('ttl') - this.divActions.classList.add('actions') + this.divSeparator.classList.add('separator') - this.divType.innerHTML = `
` this.divFQDN.innerHTML = ` *. ` - this.divActions.innerHTML = ` - - ` this.divFQDN.addEventListener('click', event => { if (event.shiftKey) @@ -512,15 +356,13 @@ class Record { else this.edit() }) + this.divValue.addEventListener('click', event => { if (event.shiftKey) this.copy(event.target.closest('.value'), this.value()) else this.edit() }) - this.divType.addEventListener('click', () => this.edit()) - this.divTTL.addEventListener('click', () => this.edit()) - this.divActions.querySelector('.delete').addEventListener('click', () => this.delete()) } // FQDN is updated. @@ -531,37 +373,14 @@ class Record { const fl = this.labels()[0] const rl = this.labels().slice(1).join('.') - const flEl = this.divFQDN.querySelector('.first-label') - const rlEl = this.divFQDN.querySelector('.rest-label') - flEl.innerText = fl - rlEl.innerText = rl != '' ? `.${rl}` : '' + this.divFQDN.querySelector('.first-label').innerText = fl + this.divFQDN.querySelector('.rest-label').innerText = rl != '' ? `.${rl}` : '' - if (rl == '_no.domain') - rlEl.classList.add('no-domain') - else - rlEl.classList.remove('no-domain') - - this.divType.querySelector('div').innerText = this.type() + this.divType.innerText = this.type() this.divValue.innerText = this.value() - this.divTTL.innerText = this.ttl() - return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions] + return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divSeparator] }// }}} - 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') - 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() == '') @@ -599,23 +418,6 @@ 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) @@ -703,8 +505,8 @@ class RecordDialog { class Settings { constructor() {// {{{ this.settings = new Map([ - ['boxed_folders', false], - ['toplevel_open', false], + ['boxed_folders', true], + ['toplevel_open', true], ]) // Read any configured settings from local storage, but keeping default value diff --git a/webserver.go b/webserver.go index fc916ec..56f75c8 100644 --- a/webserver.go +++ b/webserver.go @@ -7,7 +7,6 @@ import ( // Standard "embed" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -34,7 +33,6 @@ func registerWebserverHandlers() { http.HandleFunc("/", rootHandler) http.HandleFunc("/record/save", actionRecordSave) - http.HandleFunc("/record/delete/{id}", actionRecordDelete) } func startWebserver() { @@ -112,30 +110,6 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) { record = NewDNSRecord(entry) - 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}) + j, _ := json.Marshal(struct{ OK bool; Record DNSRecord }{true, record}) w.Write(j) }