diff --git a/config.go b/config.go index d98f201..34df751 100644 --- a/config.go +++ b/config.go @@ -3,10 +3,7 @@ package main import ( // Standard "encoding/json" - "fmt" "os" - "slices" - "strings" ) type Config struct { @@ -22,18 +19,13 @@ type Config struct { LogDir string } - Devices []Device - - filename string -} - -type Device struct { - Name string - Address string - Port int - Username string - Password string - Timeout int + Device struct { + Address string + Port int + Username string + Password string + Timeout int + } } func readConfig() (config Config, err error) { @@ -44,64 +36,5 @@ func readConfig() (config Config, err error) { } err = json.Unmarshal(configData, &config) - config.filename = flagConfig - return -} - -func (cfg *Config) UpdateDevice(currentName string, deviceToUpdate Device) (dev Device, err error) { - - i := slices.IndexFunc(cfg.Devices, func(d Device) bool { - return strings.TrimSpace(strings.ToLower(d.Name)) == strings.TrimSpace(strings.ToLower(currentName)) - }) - - if i > -1 { - dev = cfg.Devices[i] - } - - if strings.TrimSpace(deviceToUpdate.Name) == "" { - err = fmt.Errorf("Name can't be empty") - return - } - if strings.TrimSpace(deviceToUpdate.Address) == "" { - err = fmt.Errorf("Address can't be empty") - return - } - if strings.TrimSpace(deviceToUpdate.Username) == "" { - err = fmt.Errorf("Username can't be empty") - return - } - if deviceToUpdate.Port < 1 || deviceToUpdate.Port > 65535 { - err = fmt.Errorf("Invalid port") - return - } - - dev.Name = strings.TrimSpace(strings.ToLower(deviceToUpdate.Name)) - dev.Address = strings.TrimSpace(strings.ToLower(deviceToUpdate.Address)) - dev.Port = deviceToUpdate.Port - dev.Username = strings.TrimSpace(deviceToUpdate.Username) - - // TODO - Should be configurable... - if dev.Timeout == 0 { - dev.Timeout = 10 - } - - // Device not found - create it! - if i == -1 { - if strings.TrimSpace(deviceToUpdate.Password) == "" { - err = fmt.Errorf("Password can't be empty") - return - } - dev.Password = strings.TrimSpace(deviceToUpdate.Password) - cfg.Devices = append(cfg.Devices, dev) - } else { - if deviceToUpdate.Password != "" { - dev.Password = strings.TrimSpace(deviceToUpdate.Password) - } - cfg.Devices[i] = dev - } - - j, _ := json.Marshal(cfg) - err = os.WriteFile(cfg.filename, j, 0600) - return } diff --git a/main.go b/main.go index 6be7b38..2f7fbd1 100644 --- a/main.go +++ b/main.go @@ -9,8 +9,6 @@ import ( "log/slog" "os" "path" - "slices" - "fmt" ) const VERSION = "v1" @@ -25,7 +23,7 @@ var ( initLogger *slog.Logger logger *slog.Logger - devices map[string]RouterosDevice + device RouterosDevice ) func init() { // {{{ @@ -75,45 +73,13 @@ func initLogging(config Config) *slog.Logger { // {{{ func main() { initLogger.Info("application", "version", VERSION) - devices = make(map[string]RouterosDevice) - /* device.Host = config.Device.Address device.Port = config.Device.Port device.Username = config.Device.Username device.Password = config.Device.Password device.Timeout = config.Device.Timeout device.Init() - */ registerWebserverHandlers() startWebserver() } - -func routerosDevice(name string) (dev RouterosDevice, err error) { - var found bool - if dev, found = devices[name]; found { - logger.Debug("routeros", "op", "connection", "name", name, "cached", true) - return - } - - i := slices.IndexFunc(config.Devices, func(d Device) bool { - return d.Name == name - }) - if i == -1 { - err = fmt.Errorf("Unknown device '%s'", name) - logger.Error("routeros", "op", "connection", "error", err) - return - } - - logger.Debug("routeros", "name", name, "cached", false) - confDev := config.Devices[i] - dev.Host = confDev.Address - dev.Port = confDev.Port - dev.Username = confDev.Username - dev.Password = confDev.Password - dev.Timeout = confDev.Timeout - dev.Init() - devices[name] = dev - - return -} diff --git a/routeros_device.go b/routeros_device.go index 93e5738..50c19d1 100644 --- a/routeros_device.go +++ b/routeros_device.go @@ -58,7 +58,6 @@ 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) { // {{{ - logger.Debug("FOO", "port", dev.Port) url := fmt.Sprintf("https://%s:%d/rest%s", dev.Host, dev.Port, path) logger.Info("URL", "method", method, "url", url) @@ -151,7 +150,7 @@ func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (entry DNSEntry, err err = json.Unmarshal(body, &entry) return }// }}} -func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {// {{{ +func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) { _, err = dev.query("DELETE", "/ip/dns/static/"+id, []byte{}) if err != nil { rosError := struct{ Detail string }{} @@ -163,7 +162,7 @@ func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {// {{{ 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 407ae3a..ba6c2a1 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -1,7 +1,4 @@ :root { - --header-background: #e1eabe; - --header-border: 1px solid #869a41; - --line-color: #ccc; --line-color-record: #eee; @@ -23,7 +20,7 @@ --record-NXDOMAIN: #aa0000; --record-other: #888; - --record-hover: #fafafa; + --record-hover: #fffff4; } html { @@ -35,35 +32,19 @@ html { *:after { box-sizing: inherit; } -*:focus { - outline: none; -} [onClick] { cursor: pointer; } -h1 { - font-size: 1.5em; - - &:first-child { - margin-top: 0px; - } -} - label { user-select: none; } -html, -body { - margin: 0px; - padding: 0px; -} - body { font-family: sans-serif; font-size: 12pt; + margin-left: 32px; /* Boxed folders are a settings for the user. */ &.boxed-folders { @@ -107,79 +88,30 @@ select, button { font-size: 1em; padding: 4px 8px; - border-radius: 4px; - border: 1px solid #444; } -#application-header { - display: grid; - grid-template-columns: min-content min-content 1fr min-content; - align-items: center; - justify-items: end; - border-bottom: var(--header-border); - background-color: var(--header-background); +#create-icon { + position: absolute; + top: 18px; + right: 64px; + width: 24px; + cursor: pointer; +} - .device-select { - display: grid; - grid-template-columns: min-content min-content; - align-items: center; - - padding: 16px; - border-right: var(--header-border); - - .label { - grid-column: 1 / -1; - font-weight: bold; - } - - img { - margin-left: 8px; - } - } - - #search { - padding: 16px; - display: grid; - grid-template-columns: min-content min-content; - grid-gap: 0px 8px; - - - &>* { - display: none; - } - - &.show { - border-right: var(--header-border); - &>* { - display: initial; - } - } - - .search-label { - grid-column: 1 / -1; - font-weight: bold; - } - } - - #create-icon { - width: 24px; - cursor: pointer; - margin-right: 16px; - } - - #settings-icon { - width: 32px; - cursor: pointer; - margin-right: 16px; - } +#settings-icon { + position: absolute; + top: 16px; + right: 16px; + width: 32px; + cursor: pointer; +} +#search { + margin-bottom: 16px; } #records-tree { white-space: nowrap; - margin-top: calc(32px - 5px); - /* padding from the topmost folder */ - margin-left: 32px; .folder { padding-left: 32px; @@ -188,7 +120,7 @@ button { padding-left: 0px; } - &.no-domain>.label { + &.no-domain > .label { font-style: italic; } @@ -287,10 +219,6 @@ button { padding-left: 8px; border-bottom: 1px solid var(--header-line); - &.mouse-over { - background-color: var(--record-hover); - } - img { display: block; padding-left: 4px; @@ -354,8 +282,6 @@ button { border-left: 1px solid var(--header-line); align-content: center; cursor: pointer; - display: grid; - justify-items: center; &.mouse-over { background-color: var(--record-hover); @@ -474,49 +400,3 @@ button { margin-top: 8px; } } - -#device-dialog { - display: grid; - grid-template-columns: min-content min-content; - align-items: top; - grid-gap: 16px; - - .devices { - display: flex; - flex-direction:column; - gap: 8px; - align-items: flex-start; - - .header { - font-weight: bold; - } - - .device { - border: 1px solid #aaa; - background-color: #f0f0f0; - padding: 8px 16px; - border-radius: 8px; - cursor: pointer; - user-select: none; - - &.selected { - background-color: var(--header-background); - } - } - } - - .fields { - display: grid; - grid-template-columns: min-content 1fr; - grid-gap: 8px 16px; - align-items: center; - - border: 1px solid #aaa; - padding: 16px; - - .actions { - grid-column: 1 / -1; - justify-self: right; - } - } -} diff --git a/static/images/icon_create.svg b/static/images/icon_create.svg index e91d683..cc1dbd4 100644 --- a/static/images/icon_create.svg +++ b/static/images/icon_create.svg @@ -25,9 +25,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="5.6568542" - inkscape:cx="-20.948038" - inkscape:cy="9.0156115" + inkscape:zoom="1" + inkscape:cx="-71" + inkscape:cy="-52" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" @@ -62,6 +62,6 @@ + style="stroke-width:0.264583;fill:#89a02c" /> diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 707b6ed..8f9195d 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -1,109 +1,26 @@ import { MessageBus } from '@mbus' export class Application { - constructor() {// {{{ + constructor(records) {// {{{ window._mbus = new MessageBus() this.settings = new Settings() - this.devices = new Map() - this.currentDevice = null - + this.records = this.parseRecords(records) + this.topFolder = new Folder(this, null, 'root') + this.recordsTree = null this.settingsIcon = null this.createIcon = null - this.elHeader = null - this.deviceSelector = null - this.searchWidget = null - this.resetDeviceState() - this.renderApplication() - this.retrieveDevices() - .then(() => { - this.deviceSelector.setDevices(this.devices) - this.renderApplication() - }) - .catch(err => { - console.error(err) - alert(err) - }) + this.searchFor = '' - _mbus.subscribe('device_selected', event => this.connectDevice(event.detail.devName)) - _mbus.subscribe('search', ()=>this.search()) + this.renderFolders() + this.render() }// }}} - - // resetDeviceState removes the device state such as the records, records tree and search field. - resetDeviceState() {// {{{ - if (this.recordsTree) { - this.recordsTree.remove() - this.recordsTree = null - return - } - - this.topFolder = new Folder(this, null, 'root') - this.records = null - this.recordsTree = null - - if (this.searchWidget) - this.searchWidget.reset() - }// }}} - - // retrieveDevices fetches the preconfigured devices from server and populates this.devices. - retrieveDevices() {// {{{ - return new Promise((resolve, reject) => { - fetch('/devices') - .then(data => data.json()) - .then(json => { - if (!json.OK) { - reject(json.Error) - return - } - - this.devices = new Map() - for (const devData of json.Devices) { - const dev = new Device(devData) - this.devices.set(dev.name(), dev) - } - - resolve() - }) - }) - }// }}} - - // connectDevice resets the device state, retrieves the records and renders everything necessary. - async connectDevice(devName) {// {{{ - this.resetDeviceState() - if (devName == '-') { - this.settings.set('last_device', '') - return - } - - try { - const dev = this.devices.get(devName) - if (dev === undefined) { - alert(`Unknown device '${devName}'`) - return - } - this.currentDevice = dev - - const records = await this.currentDevice.retrieveRecords() - this.records = this.parseRecords(records) - - this.settings.set('last_device', this.currentDevice.name()) - - this.createFolders() - this.renderDevice() - } catch (err) { - console.error(err) - alert(err) - } - }// }}} - - // filteredRecords takes the current search value and only returns records matching it. filteredRecords() {// {{{ - const searchFor = this.searchWidget.value() - if (searchFor === '') + if (this.searchFor === '') return this.records const records = this.records.filter(r => { - return (r.name().includes(searchFor)) + return (r.name().includes(this.searchFor)) }) return records }// }}} @@ -113,7 +30,7 @@ export class Application { }// }}} // cleanFolders removes all records from all folders. - // createFolders can then put moved records in the correct + // renderFolders can then put moved records in the correct // (or newly created) folders again when record names are updated. cleanFolders(folder) {// {{{ if (folder === undefined) @@ -128,99 +45,7 @@ export class Application { const i = this.records.findIndex(rec => rec.id() == id) this.records.splice(i, 1) }// }}} - sortFolders(a, b) {// {{{ - const aLabels = a.labels().reverse() - const bLabels = b.labels().reverse() - - for (let i = 0; i < aLabels.length && i < bLabels.length; i++) { - if (aLabels[i] < bLabels[i]) - return -1 - if (aLabels[i] > bLabels[i]) - return 1 - } - - if (a.length < b.length) return 1 - if (a.length > b.length) return -1 - - return 0 - }// }}} - sortRecords(a, b) {// {{{ - const aLabels = a.labels().reverse() - const bLabels = b.labels().reverse() - - for (let i = 0; i < aLabels.length && i < bLabels.length; i++) { - if (aLabels[i] < bLabels[i]) - return -1 - if (aLabels[i] > bLabels[i]) - return 1 - } - - return 0 - }// }}} - - // renderApplication creates and updates application-level elements (not device level elements). - renderApplication() {// {{{ - if (this.elHeader === null) { - this.elHeader = document.createElement('div') - this.elHeader.id = 'application-header' - - this.deviceSelector = new DeviceSelectWidget(this.devices) - this.searchWidget = new SearchWidget() - - this.createIcon = document.createElement('img') - this.createIcon.id = 'create-icon' - this.createIcon.src = `/images/${_VERSION}/icon_create.svg` - this.createIcon.addEventListener('click', () => new RecordDialog(new Record()).show()) - - this.settingsIcon = document.createElement('img') - this.settingsIcon.id = 'settings-icon' - this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg` - this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show()) - - this.elHeader.appendChild(this.deviceSelector.render()) - this.elHeader.appendChild(this.searchWidget.render()) - this.elHeader.appendChild(this.createIcon) - this.elHeader.appendChild(this.settingsIcon) - - document.getElementById('app').appendChild(this.elHeader) - document.body.addEventListener('keydown', event => this.handlerKeys(event)) - return - } - - this.deviceSelector.render() - const lastDevice = this.settings.get('last_device') - if (lastDevice !== '') - this.deviceSelector.selectDevice(lastDevice) - }// }}} - - // renderDevice creates the device specific elements and also updates them. - renderDevice() {// {{{ - // 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.getElementById('app').appendChild(this.recordsTree) - } - - // Top root folder doesn't have to be shown. - const folders = Array.from(this.topFolder.subfolders.values()) - folders.sort(this.sortFolders) - - 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')) - }// }}} - - // createFolders goes through the filtered records and create new folders based on the record labels. - // It also populates the child records. - createFolders() {// {{{ + renderFolders() {// {{{ const records = this.filteredRecords() records.sort(this.sortRecords) @@ -260,9 +85,91 @@ export class Application { currFolder.addRecord(rec) } }// }}} + sortFolders(a, b) {// {{{ + const aLabels = a.labels().reverse() + const bLabels = b.labels().reverse() - // removeEmptyFolders finds the leaf folders and recursively removes empty ones. - // These are usually left from moving records away from them. + for (let i = 0; i < aLabels.length && i < bLabels.length; i++) { + if (aLabels[i] < bLabels[i]) + return -1 + if (aLabels[i] > bLabels[i]) + return 1 + } + + if (a.length < b.length) return 1 + if (a.length > b.length) return -1 + + return 0 + }// }}} + sortRecords(a, b) {// {{{ + const aLabels = a.labels().reverse() + const bLabels = b.labels().reverse() + + for (let i = 0; i < aLabels.length && i < bLabels.length; i++) { + if (aLabels[i] < bLabels[i]) + return -1 + if (aLabels[i] > bLabels[i]) + return 1 + } + + return 0 + }// }}} + render() {// {{{ + if (this.createIcon === null) { + this.createIcon = document.createElement('img') + this.createIcon.id = 'create-icon' + this.createIcon.src = `/images/${_VERSION}/icon_create.svg` + this.createIcon.addEventListener('click', () => new RecordDialog(new Record()).show()) + + this.settingsIcon = document.createElement('img') + this.settingsIcon.id = 'settings-icon' + 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.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) + + 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 @@ -282,7 +189,6 @@ export class Application { folder.div?.remove() } }// }}} - handlerKeys(event) {// {{{ let handled = true @@ -300,11 +206,7 @@ export class Application { break case 'f': - this.searchWidget.searchField.focus() - break - - case 'd': - new DeviceDialog(this.devices).render() + this.searchField.focus() break default: @@ -327,145 +229,15 @@ export class Application { else document.body.classList.remove('boxed-folders') }// }}} - - // search sets the search filter and re-renders the records tree. search() {// {{{ + this.searchFor = this.searchField.value.trim().toLowerCase() this.recordsTree.remove() this.topFolder = new Folder(this, null, 'root') this.recordsTree = null - this.createFolders() - this.topFolder.openFolder(true) - this.renderDevice() - }// }}} -} - -class SearchWidget { - constructor() {// {{{ - _mbus.subscribe('device_selected', event => { - if (event.detail.devName == '-') - this.searchEl?.classList.remove('show') - else - this.searchEl?.classList.add('show') - }) - }// }}} - render() {// {{{ - if (this.searchEl) - return - - this.searchEl = document.createElement('div') - this.searchEl.id = 'search' - this.searchEl.innerHTML = ` -
Search
- - - ` - this.searchField = this.searchEl.querySelector('.search-for') - this.searchField.addEventListener('keydown', event => { - if (event.key == 'Enter') - this.search() - }) - - const searchButton = this.searchEl.querySelector('button.search') - searchButton.addEventListener('click', () => this.search()) - - return this.searchEl - }// }}} - search() {// {{{ - _mbus.dispatch('search', { search: this.searchField.value.trim().toLowerCase() }) - }// }}} - value() {// {{{ - return this.searchField.value.trim().toLowerCase() - }// }}} - reset() {// {{{ - this.searchField.value = '' - }// }}} -} - -class Device { - constructor(data) {// {{{ - this.data = data - }// }}} - name() {// {{{ - return this.data.Name?.toLowerCase() || '' - }// }}} - address() {// {{{ - return this.data.Address || '' - }// }}} - port() {// {{{ - return parseInt(this.data.Port || 0) - }// }}} - username() {// {{{ - return this.data.Username || '' - }// }}} - password() {// {{{ - return this.data.Password || '' - }// }}} - name() {// {{{ - return this.data.Name?.toLowerCase() || '' - }// }}} - async retrieveRecords() {// {{{ - const data = await fetch(`/device/${this.name()}/dns_records`) - const json = await data.json() - if (!json.OK) - throw new Error(json.Error) - return json.Records - }// }}} -} - -class DeviceSelectWidget { - constructor(devices) {// {{{ - this.devices = devices - - this.div = null - this.elDeviceSelect = null - this.currentlySelected = null - }// }}} - setDevices(devices) {// {{{ - this.devices = devices - }// }}} - render() {// {{{ - if (this.div === null) { - this.div = document.createElement('div') - this.div.classList.add('device-select') - this.div.innerHTML = ` -
Select device
- - - ` - - this.elDeviceSelect = this.div.querySelector('select') - this.elDeviceSelect.addEventListener('change', () => this.notifyDeviceSelect()) - this.div.querySelector('img').addEventListener('click', ()=>new DeviceDialog(this.devices).render()) - } - this.restockDeviceSelect() - return this.div - }// }}} - restockDeviceSelect() {// {{{ - this.elDeviceSelect.replaceChildren() - const emptyOption = document.createElement('option') - emptyOption.innerText = "-" - emptyOption.dataset.devicename = "-" - this.elDeviceSelect.appendChild(emptyOption) - - this.devices.forEach(dev => { - const option = document.createElement('option') - option.innerText = dev.name() - option.dataset.devicename = dev.name() - this.elDeviceSelect.appendChild(option) - }) - - if (this.currentlySelected !== null) - this.elDeviceSelect.value = this.currentlySelected - }// }}} - notifyDeviceSelect() {// {{{ - const devName = this.elDeviceSelect.value - this.currentlySelected = devName - _mbus.dispatch('device_selected', { devName }) - }// }}} - selectDevice(devname) {// {{{ - this.elDeviceSelect.value = devname - this.notifyDeviceSelect() + this.cleanFolders() + this.renderFolders() + this.render() }// }}} } @@ -562,8 +334,10 @@ class Folder { 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') + if (this.name() == '_no.domain') { + console.log('wut') this.div.classList.add('no-domain') + } } @@ -774,7 +548,6 @@ class Record { return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions] }// }}} mouseEnter() {// {{{ - this.imgIcon.classList.add('mouse-over') this.divFQDN.classList.add('mouse-over') this.divType.classList.add('mouse-over') this.divValue.classList.add('mouse-over') @@ -782,7 +555,6 @@ class Record { this.divActions.classList.add('mouse-over') }// }}} mouseLeave() {// {{{ - this.imgIcon.classList.remove('mouse-over') this.divFQDN.classList.remove('mouse-over') this.divType.classList.remove('mouse-over') this.divValue.classList.remove('mouse-over') @@ -793,7 +565,7 @@ class Record { save() {// {{{ const created = (this.id() == '') - fetch(`/device/${_app.currentDevice.name()}/record`, { + fetch('/record/save', { method: 'POST', body: JSON.stringify(this.data), }) @@ -812,10 +584,10 @@ class Record { } _app.cleanFolders() - _app.createFolders() - _app.renderDevice() + _app.renderFolders() + _app.render() - // createFolders is setting the folder the record resides in. + // renderFolders is setting the folder the record resides in. // It can now be expanded to the parent folder. if (created) { this.openParentFolders() @@ -831,7 +603,7 @@ class Record { if (!confirm(`Are you sure you want to delete ${this.name()}?`)) return - fetch(`/device/${_app.currentDevice.name()}/record/${this.id()}`, { method: 'DELETE' }) + fetch(`/record/delete/${this.id()}`) .then(data => data.json()) .then(json => { if (!json.OK) { @@ -840,8 +612,8 @@ class Record { } _app.deleteRecord(this.id()) _app.cleanFolders() - _app.createFolders() - _app.renderDevice() + _app.renderFolders() + _app.render() }) }// }}} @@ -933,7 +705,6 @@ class Settings { this.settings = new Map([ ['boxed_folders', false], ['toplevel_open', false], - ['last_device', ''], ]) // Read any configured settings from local storage, but keeping default value @@ -997,117 +768,3 @@ class SettingsDialog { this.dlg.close() }// }}} } - -class DeviceDialog { - constructor(devices) {// {{{ - this.devices = devices - this.device = null - }// }}} - render() {// {{{ - // Only one open at any one time. - if (document.getElementById('device-dialog')) - return - - this.dlg = document.createElement('dialog') - this.dlg.id = 'device-dialog' - this.dlg.innerHTML = ` -
-
Devices
-
-
-
Name
- - -
Address
- - -
Port
- - -
Username
- - -
Password
- - -
- - -
-
- ` - - this.elDevices = this.dlg.querySelector('.devices') - this.elFields = this.dlg.querySelector('.fields') - this.elName = this.dlg.querySelector('.fields .name') - this.elAddress = this.dlg.querySelector('.fields .address') - this.elPort = this.dlg.querySelector('.fields .port') - this.elUsername = this.dlg.querySelector('.fields .username') - this.elPassword = this.dlg.querySelector('.fields .password') - - const devices = Array.from(this.devices.values()) - devices.sort((a, b)=>{ - if (a.name() < b.name()) return -1 - if (a.name() > b.name()) return 1 - return 0 - }) - devices.forEach(dev=>{ - const devEl = document.createElement('div') - devEl.classList.add('device') - devEl.innerText = dev.name() - devEl.addEventListener('click', ()=>this.editDevice(dev, devEl)) - this.elDevices.appendChild(devEl) - }) - - this.dlg.querySelector('.update').addEventListener('click', ()=>this.updateDevice()) - this.dlg.addEventListener('close', ()=>this.dlg.remove()) - - document.body.appendChild(this.dlg) - this.dlg.showModal() - }// }}} - - editDevice(dev, devEl) {// {{{ - this.device = dev - - this.elDevices.querySelectorAll('.device.selected').forEach(el=>el.classList.remove('selected')) - devEl.classList.add('selected') - - this.elName.value = dev.name() - this.elAddress.value = dev.address() - this.elPort.value = dev.port() - this.elUsername.value = dev.username() - this.elPassword.value = dev.password() - }// }}} - async updateDevice() {// {{{ - const req = { - CurrentName: this.device.name(), - Device: { - Name: this.elName.value, - Address: this.elAddress.value, - Port: parseInt(this.elPort.value), - Username: this.elUsername.value, - Password: this.elPassword.value, - } - } - - try { - const data = await fetch('/device', { - method: 'POST', - body: JSON.stringify(req), - }) - const json = await data.json() - - this.device.data.Name = json.Device.Name - this.device.data.Address = json.Device.Address - this.device.data.Port = json.Device.Port - this.device.data.Username = json.Device.Username - - _mbus.dispatch('device_updated', { device: this.device }) - this.dlg.close() - - } catch(err) { - console.error(err) - alert(err) - } - }// }}} -} diff --git a/views/pages/index.gotmpl b/views/pages/index.gotmpl index f774e4f..d045df4 100644 --- a/views/pages/index.gotmpl +++ b/views/pages/index.gotmpl @@ -1,6 +1,9 @@ {{ define "page" }} + +

{{ .Data.Identity }}

+ {{ end }} diff --git a/webserver.go b/webserver.go index a87a3d3..fc916ec 100644 --- a/webserver.go +++ b/webserver.go @@ -24,7 +24,7 @@ var ( viewFS embed.FS ) -func registerWebserverHandlers() { // {{{ +func registerWebserverHandlers() { var err error htmlEngine, err = HTMLTemplate.NewEngine(viewFS, staticFS, flagDev) if err != nil { @@ -33,32 +33,17 @@ func registerWebserverHandlers() { // {{{ } http.HandleFunc("/", rootHandler) - http.HandleFunc("GET /devices", actionDevices) - http.HandleFunc("POST /device", actionDeviceUpdate) - http.HandleFunc("GET /device/{dev}/dns_records", actionDNSRecords) - http.HandleFunc("POST /device/{dev}/record", actionRecordSave) - http.HandleFunc("DELETE /device/{dev}/record/{id}", actionRecordDelete) -} // }}} -func startWebserver() { // {{{ + http.HandleFunc("/record/save", actionRecordSave) + http.HandleFunc("/record/delete/{id}", actionRecordDelete) +} + +func startWebserver() { listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port) logger.Info("webserver", "listen", listen) http.ListenAndServe(listen, nil) -} // }}} +} -func httpError(w http.ResponseWriter, err error) { // {{{ - resp := struct { - OK bool - Error string - }{ - false, - err.Error(), - } - - j, _ := json.Marshal(resp) - w.Write(j) -} // }}} - -func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ +func rootHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { page := HTMLTemplate.SimplePage{} page.Layout = "main" @@ -67,6 +52,21 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ var err error data := make(map[string]any) + data["Identity"], err = device.GetIdentity() + if err != nil { + w.Write([]byte(err.Error())) + return + } + + var entries []DNSRecord + entries, err = device.StaticDNSEntries() + if err != nil { + w.Write([]byte(err.Error())) + return + } + + data["DNSRecords"] = entries + data["VERSION"] = VERSION page.Data = data @@ -79,91 +79,25 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ } htmlEngine.StaticResource(w, r) -} // }}} -func actionDevices(w http.ResponseWriter, r *http.Request) { // {{{ - var devs []Device - for _, dev := range config.Devices { - dev.Password = "" - devs = append(devs, dev) - } +} - j, _ := json.Marshal(struct { - OK bool - Devices []Device +func httpError(w http.ResponseWriter, err error) { + resp := struct { + OK bool + Error string }{ - true, - devs, - }) + false, + err.Error(), + } + j, _ := json.Marshal(resp) w.Write(j) -} // }}} -func actionDeviceUpdate(w http.ResponseWriter, r *http.Request) { // {{{ - var req struct { - CurrentName string - Device Device - } - - body, _ := io.ReadAll(r.Body) - err := json.Unmarshal(body, &req) - if err != nil { - httpError(w, err) - return - } - - device, err := config.UpdateDevice(req.CurrentName, req.Device) - if err != nil { - httpError(w, err) - return - } - device.Password = "" // don't leak unnecessarily - - j, _ := json.Marshal(struct { - OK bool - Device Device - }{ - true, - device, - }) - w.Write(j) -} // }}} - -func actionDNSRecords(w http.ResponseWriter, r *http.Request) { // {{{ - devname := r.PathValue("dev") - - device, err := routerosDevice(devname) - if err != nil { - httpError(w, err) - return - } - - var records []DNSRecord - records, err = device.StaticDNSEntries() - if err != nil { - httpError(w, err) - return - } - - j, _ := json.Marshal(struct { - OK bool - Records []DNSRecord - }{ - true, - records, - }) - w.Write(j) - -} // }}} -func actionRecordSave(w http.ResponseWriter, r *http.Request) { // {{{ - devName := r.PathValue("dev") - device, err := routerosDevice(devName) - if err != nil { - httpError(w, err) - return - } +} +func actionRecordSave(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) var record DNSRecord - err = json.Unmarshal(body, &record) + err := json.Unmarshal(body, &record) if err != nil { httpError(w, err) return @@ -183,22 +117,17 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) { // {{{ Record DNSRecord }{true, record}) w.Write(j) -} // }}} -func actionRecordDelete(w http.ResponseWriter, r *http.Request) { // {{{ - devName := r.PathValue("dev") - device, err := routerosDevice(devName) - if err != nil { - httpError(w, err) - return - } +} +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) + err := device.DeleteDNSEntry(id) if err != nil { httpError(w, err) logger.Error("webserver", "op", "record_delete", "error", err) @@ -206,7 +135,7 @@ func actionRecordDelete(w http.ResponseWriter, r *http.Request) { // {{{ } j, _ := json.Marshal(struct { - OK bool + OK bool }{true}) w.Write(j) -} // }}} +}