diff --git a/config.go b/config.go
index 34df751..1293c5f 100644
--- a/config.go
+++ b/config.go
@@ -19,13 +19,16 @@ type Config struct {
LogDir string
}
- Device struct {
- Address string
- Port int
- Username string
- Password string
- Timeout int
- }
+ Devices []Device
+}
+
+type Device struct {
+ Name string
+ Address string
+ Port int
+ Username string
+ Password string
+ Timeout int
}
func readConfig() (config Config, err error) {
diff --git a/main.go b/main.go
index 2f7fbd1..6be7b38 100644
--- a/main.go
+++ b/main.go
@@ -9,6 +9,8 @@ import (
"log/slog"
"os"
"path"
+ "slices"
+ "fmt"
)
const VERSION = "v1"
@@ -23,7 +25,7 @@ var (
initLogger *slog.Logger
logger *slog.Logger
- device RouterosDevice
+ devices map[string]RouterosDevice
)
func init() { // {{{
@@ -73,13 +75,45 @@ 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/static/css/index.css b/static/css/index.css
index ba6c2a1..f28e824 100644
--- a/static/css/index.css
+++ b/static/css/index.css
@@ -1,4 +1,6 @@
:root {
+ --header-background: #acbb78;
+
--line-color: #ccc;
--line-color-record: #eee;
@@ -37,14 +39,27 @@ html {
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 {
@@ -90,28 +105,63 @@ button {
padding: 4px 8px;
}
-#create-icon {
- position: absolute;
- top: 18px;
- right: 64px;
- width: 24px;
- cursor: pointer;
-}
+#application-header {
+ display: grid;
+ grid-template-columns: min-content 1fr min-content min-content;
+ align-items: center;
+ border-bottom: 1px solid #a4bc52;
+ background-color: var(--header-background);
-#settings-icon {
- position: absolute;
- top: 16px;
- right: 16px;
- width: 32px;
- cursor: pointer;
-}
-#search {
- margin-bottom: 16px;
+ .device-select {
+ padding: 16px 16px 16px 32px;
+
+ .device-name {
+ font-weight: bold;
+ }
+ }
+
+ #search {
+ padding: 16px;
+ display: grid;
+ grid-template-columns: min-content min-content;
+ grid-gap: 0px 8px;
+
+ &>* {
+ display: none;
+ }
+
+ &.show {
+ &>* {
+ 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;
+ }
+
}
#records-tree {
white-space: nowrap;
+ margin-top: calc(32px - 5px);
+ /* padding from the topmost folder */
+ margin-left: 32px;
.folder {
padding-left: 32px;
@@ -120,7 +170,7 @@ button {
padding-left: 0px;
}
- &.no-domain > .label {
+ &.no-domain>.label {
font-style: italic;
}
diff --git a/static/images/icon_create.svg b/static/images/icon_create.svg
index cc1dbd4..1fb9dea 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="1"
- inkscape:cx="-71"
- inkscape:cy="-52"
+ inkscape:zoom="1.1646825"
+ inkscape:cx="-71.693356"
+ inkscape:cy="-55.809199"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
@@ -62,6 +62,6 @@
+ style="stroke-width:0.264583;fill:#000000" />
diff --git a/static/js/dns.mjs b/static/js/dns.mjs
index 8f9195d..08d9302 100644
--- a/static/js/dns.mjs
+++ b/static/js/dns.mjs
@@ -1,26 +1,105 @@
import { MessageBus } from '@mbus'
export class Application {
- constructor(records) {// {{{
+ constructor() {// {{{
window._mbus = new MessageBus()
this.settings = new Settings()
- this.records = this.parseRecords(records)
- this.topFolder = new Folder(this, null, 'root')
- this.recordsTree = null
+ this.devices = new Map()
+ this.currentDevice = null
+
this.settingsIcon = null
this.createIcon = null
+ this.elHeader = null
+ this.deviceSelector = null
+ this.searchWidget = null
- this.searchFor = ''
+ this.resetDeviceState()
+ this.renderApplication()
+ this.retrieveDevices()
+ .then(() => {
+ this.deviceSelector.setDevices(this.devices)
+ this.renderApplication()
+ })
+ .catch(err => {
+ console.error(err)
+ alert(err)
+ })
- this.renderFolders()
- this.render()
+ _mbus.subscribe('device_selected', event => this.connectDevice(event.detail.devName))
+ _mbus.subscribe('search', ()=>this.search())
}// }}}
+
+ // 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 == '-') {
+ 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.createFolders()
+ this.renderDevice()
+ } catch (err) {
+ console.error(err)
+ alert(err)
+ }
+ }// }}}
+
+ // filteredRecords takes the current search value and only returns records matching it.
filteredRecords() {// {{{
- if (this.searchFor === '')
+ const searchFor = this.searchWidget.value()
+ if (searchFor === '')
return this.records
const records = this.records.filter(r => {
- return (r.name().includes(this.searchFor))
+ return (r.name().includes(searchFor))
})
return records
}// }}}
@@ -30,7 +109,7 @@ export class Application {
}// }}}
// cleanFolders removes all records from all folders.
- // renderFolders can then put moved records in the correct
+ // createFolders can then put moved records in the correct
// (or newly created) folders again when record names are updated.
cleanFolders(folder) {// {{{
if (folder === undefined)
@@ -45,7 +124,96 @@ export class Application {
const i = this.records.findIndex(rec => rec.id() == id)
this.records.splice(i, 1)
}// }}}
- renderFolders() {// {{{
+ 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()
+ }// }}}
+
+ // 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() {// {{{
const records = this.filteredRecords()
records.sort(this.sortRecords)
@@ -85,91 +253,9 @@ export class Application {
currFolder.addRecord(rec)
}
}// }}}
- 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
- }// }}}
- 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 finds the leaf folders and recursively removes empty ones.
+ // These are usually left from moving records away from them.
removeEmptyFolders(folder) {// {{{
if (folder === undefined)
folder = this.topFolder
@@ -189,6 +275,7 @@ export class Application {
folder.div?.remove()
}
}// }}}
+
handlerKeys(event) {// {{{
let handled = true
@@ -229,15 +316,124 @@ 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.cleanFolders()
- this.renderFolders()
- this.render()
+ this.createFolders()
+ 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() || ''
+ }// }}}
+ 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.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 })
}// }}}
}
@@ -334,10 +530,8 @@ 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') {
- console.log('wut')
+ if (this.name() == '_no.domain')
this.div.classList.add('no-domain')
- }
}
@@ -565,7 +759,7 @@ class Record {
save() {// {{{
const created = (this.id() == '')
- fetch('/record/save', {
+ fetch(`/device/${_app.currentDevice.name()}/record`, {
method: 'POST',
body: JSON.stringify(this.data),
})
@@ -584,10 +778,10 @@ class Record {
}
_app.cleanFolders()
- _app.renderFolders()
- _app.render()
+ _app.createFolders()
+ _app.renderDevice()
- // renderFolders is setting the folder the record resides in.
+ // createFolders is setting the folder the record resides in.
// It can now be expanded to the parent folder.
if (created) {
this.openParentFolders()
@@ -603,7 +797,7 @@ class Record {
if (!confirm(`Are you sure you want to delete ${this.name()}?`))
return
- fetch(`/record/delete/${this.id()}`)
+ fetch(`/device/${_app.currentDevice.name()}/record/${this.id()}`, { method: 'DELETE' })
.then(data => data.json())
.then(json => {
if (!json.OK) {
@@ -612,8 +806,8 @@ class Record {
}
_app.deleteRecord(this.id())
_app.cleanFolders()
- _app.renderFolders()
- _app.render()
+ _app.createFolders()
+ _app.renderDevice()
})
}// }}}
diff --git a/views/pages/index.gotmpl b/views/pages/index.gotmpl
index d045df4..f774e4f 100644
--- a/views/pages/index.gotmpl
+++ b/views/pages/index.gotmpl
@@ -1,9 +1,6 @@
{{ define "page" }}
-
-{{ .Data.Identity }}
-
{{ end }}
diff --git a/webserver.go b/webserver.go
index fc916ec..0869f7c 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,17 +33,31 @@ func registerWebserverHandlers() {
}
http.HandleFunc("/", rootHandler)
- http.HandleFunc("/record/save", actionRecordSave)
- http.HandleFunc("/record/delete/{id}", actionRecordDelete)
-}
-
-func startWebserver() {
+ http.HandleFunc("/devices", actionDevices)
+ 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() { // {{{
listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
logger.Info("webserver", "listen", listen)
http.ListenAndServe(listen, nil)
-}
+} // }}}
-func rootHandler(w http.ResponseWriter, r *http.Request) {
+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) { // {{{
if r.URL.Path == "/" {
page := HTMLTemplate.SimplePage{}
page.Layout = "main"
@@ -52,21 +66,6 @@ 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,25 +78,62 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
}
htmlEngine.StaticResource(w, r)
-}
-
-func httpError(w http.ResponseWriter, err error) {
- resp := struct {
- OK bool
- Error string
- }{
- false,
- err.Error(),
+} // }}}
+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(resp)
- w.Write(j)
-}
+ j, _ := json.Marshal(struct {
+ OK bool
+ Devices []Device
+ }{
+ true,
+ devs,
+ })
+
+ 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
@@ -117,17 +153,22 @@ 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)
@@ -135,7 +176,7 @@ func actionRecordDelete(w http.ResponseWriter, r *http.Request) {
}
j, _ := json.Marshal(struct {
- OK bool
+ OK bool
}{true})
w.Write(j)
-}
+} // }}}