- switched all admin routes to /admin/...

- auto redirect from ./ to ./admin
- enabled two auth flows (admin and update)
- disabled auth for admin by skipping env variable DDNS_ADMIN_LOGIN
- introduced optional env variable DDNS_TITLE for dynamic UI title (default TheBBCloudDynDNS)
- set copyright date in footer dynamic on startup
- moved all remote js/css packages into static in order to avoid external dependencies
- added "copy to clipboard button" on host overview page
- replaced all fmt.Println to log...
- introduced new optional env variable DDNS_CLEAR_LOG_INTERVAL to clear logs after n days (int). (check runs daily once if update request received)
- newest logs are shown from top to button on logs page
This commit is contained in:
Malte
2022-04-04 13:03:25 +02:00
parent fcb7f88507
commit d84c3352a9
20 changed files with 291 additions and 108 deletions
+2
View File
@@ -24,6 +24,7 @@ func (h *Handler) ListCNames(c echo.Context) (err error) {
return c.Render(http.StatusOK, "listcnames", echo.Map{
"cnames": cnames,
"title": h.Title,
})
}
@@ -42,6 +43,7 @@ func (h *Handler) AddCName(c echo.Context) (err error) {
return c.Render(http.StatusOK, "addcname", echo.Map{
"config": h.Config,
"hosts": hosts,
"title": h.Title,
})
}
+69 -21
View File
@@ -2,8 +2,12 @@ package handler
import (
"fmt"
"github.com/labstack/gommon/log"
"os"
"strconv"
"strings"
"time"
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
"github.com/go-playground/validator/v10"
@@ -13,10 +17,14 @@ import (
)
type Handler struct {
DB *gorm.DB
AuthHost *model.Host
AuthAdmin bool
Config Envs
DB *gorm.DB
AuthHost *model.Host
AuthAdmin bool
Config Envs
Title string
DisableAdminAuth bool
LastClearedLogs time.Time
ClearInterval uint64
}
type Envs struct {
@@ -39,13 +47,25 @@ type Error struct {
// Authenticate is the method the website admin user and the host update user have to authenticate against.
// To gather admin rights the username password combination must match with the credentials given by the env var.
func (h *Handler) Authenticate(username, password string, c echo.Context) (bool, error) {
func (h *Handler) AuthenticateUpdate(username, password string, c echo.Context) (bool, error) {
h.CheckClearInterval()
h.AuthHost = nil
h.AuthAdmin = false
host := &model.Host{}
if err := h.DB.Where(&model.Host{UserName: username, Password: password}).First(host).Error; err != nil {
log.Error("Error:", err)
return false, nil
}
h.AuthHost = host
return true, nil
}
func (h *Handler) AuthenticateAdmin(username, password string, c echo.Context) (bool, error) {
h.AuthAdmin = false
ok, err := h.authByEnv(username, password)
if err != nil {
fmt.Println("Error:", err)
log.Error("Error:", err)
return false, nil
}
@@ -54,17 +74,8 @@ func (h *Handler) Authenticate(username, password string, c echo.Context) (bool,
return true, nil
}
host := &model.Host{}
if err := h.DB.Where(&model.Host{UserName: username, Password: password}).First(host).Error; err != nil {
fmt.Println("Error:", err)
return false, nil
}
h.AuthHost = host
return true, nil
return false, nil
}
func (h *Handler) authByEnv(username, password string) (bool, error) {
hashReader := strings.NewReader(h.Config.AdminLogin)
@@ -83,19 +94,40 @@ func (h *Handler) authByEnv(username, password string) (bool, error) {
// ParseEnvs parses all needed environment variables:
// DDNS_ADMIN_LOGIN: The basic auth login string in htpasswd style.
// DDNS_DOMAINS: All domains that will be handled by the dyndns server.
func (h *Handler) ParseEnvs() error {
func (h *Handler) ParseEnvs() (adminAuth bool, err error) {
log.Info("Read environment variables")
h.Config = Envs{}
adminAuth = true
h.Config.AdminLogin = os.Getenv("DDNS_ADMIN_LOGIN")
if h.Config.AdminLogin == "" {
return fmt.Errorf("environment variable DDNS_ADMIN_LOGIN has to be set")
log.Info("No Auth! DDNS_ADMIN_LOGIN should be set")
adminAuth = false
h.AuthAdmin = true
h.DisableAdminAuth = true
}
h.Title = os.Getenv("DDNS_TITLE")
if h.Title == "" {
h.Title = "TheBBCloud DynDNS"
}
clearEnv := os.Getenv("DDNS_CLEAR_LOG_INTERVAL")
clearInterval, err := strconv.ParseUint(clearEnv, 10, 32)
if err != nil {
log.Info("No log clear interval found")
} else {
log.Info("log clear interval found:", clearInterval, "days")
h.ClearInterval = clearInterval
if clearInterval > 0 {
h.LastClearedLogs = time.Now()
}
}
h.Config.Domains = strings.Split(os.Getenv("DDNS_DOMAINS"), ",")
if len(h.Config.Domains) < 1 {
return fmt.Errorf("environment variable DDNS_DOMAINS has to be set")
return adminAuth, fmt.Errorf("environment variable DDNS_DOMAINS has to be set")
}
return nil
return adminAuth, nil
}
// InitDB creates an empty database and creates all tables if there isn't already one, or opens the existing one.
@@ -126,3 +158,19 @@ func (h *Handler) InitDB() (err error) {
return nil
}
// Check if a log cleaning is needed
func (h *Handler) CheckClearInterval() {
if !h.LastClearedLogs.IsZero() {
if !DateEqual(time.Now(), h.LastClearedLogs) {
go h.ClearLogs()
}
}
}
// compare two dates
func DateEqual(date1, date2 time.Time) bool {
y1, m1, d1 := date1.Date()
y2, m2, d2 := date2.Date()
return y1 == y2 && m1 == m2 && d1 == d2
}
+11 -7
View File
@@ -2,6 +2,7 @@ package handler
import (
"fmt"
l "github.com/labstack/gommon/log"
"net"
"net/http"
"strconv"
@@ -51,6 +52,7 @@ func (h *Handler) ListHosts(c echo.Context) (err error) {
return c.Render(http.StatusOK, "listhosts", echo.Map{
"hosts": hosts,
"title": h.Title,
})
}
@@ -63,6 +65,7 @@ func (h *Handler) AddHost(c echo.Context) (err error) {
return c.Render(http.StatusOK, "edithost", echo.Map{
"addEdit": "add",
"config": h.Config,
"title": h.Title,
})
}
@@ -86,6 +89,7 @@ func (h *Handler) EditHost(c echo.Context) (err error) {
"host": host,
"addEdit": "edit",
"config": h.Config,
"title": h.Title,
})
}
@@ -109,7 +113,7 @@ func (h *Handler) CreateHost(c echo.Context) (err error) {
if err = h.checkUniqueHostname(host.Hostname, host.Domain); err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
host.LastUpdate = time.Now()
if err = h.DB.Create(host).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
@@ -237,7 +241,7 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
if err != nil {
log.Message = "Bad Request: Unable to get caller IP"
if err = h.CreateLogEntry(log); err != nil {
fmt.Println(err)
l.Error(err)
}
return c.String(http.StatusBadRequest, "badrequest\n")
@@ -249,7 +253,7 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
if hostname == "" || hostname != h.AuthHost.Hostname+"."+h.AuthHost.Domain {
log.Message = "Hostname or combination of authenticated user and hostname is invalid"
if err = h.CreateLogEntry(log); err != nil {
fmt.Println(err)
l.Error(err)
}
return c.String(http.StatusBadRequest, "notfqdn\n")
@@ -263,7 +267,7 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
if ipType == "" {
log.Message = "Bad Request: Sent IP is invalid"
if err = h.CreateLogEntry(log); err != nil {
fmt.Println(err)
l.Error(err)
}
return c.String(http.StatusBadRequest, "badrequest\n")
@@ -273,10 +277,10 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
// add/update DNS record
if err = nswrapper.UpdateRecord(log.Host.Hostname, log.SentIP, ipType, log.Host.Domain, log.Host.Ttl); err != nil {
log.Message = fmt.Sprintf("DNS error: %v", err)
l.Error(log.Message)
if err = h.CreateLogEntry(log); err != nil {
fmt.Println(err)
l.Error(err)
}
return c.String(http.StatusBadRequest, "dnserr\n")
}
@@ -285,7 +289,7 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
log.Status = true
log.Message = "No errors occurred"
if err = h.CreateLogEntry(log); err != nil {
fmt.Println(err)
l.Error(err)
}
return c.String(http.StatusOK, "good\n")
+15 -4
View File
@@ -1,8 +1,10 @@
package handler
import (
"log"
"net/http"
"strconv"
"time"
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
"github.com/labstack/echo/v4"
@@ -24,12 +26,13 @@ func (h *Handler) ShowLogs(c echo.Context) (err error) {
}
logs := new([]model.Log)
if err = h.DB.Preload("Host").Limit(30).Find(logs).Error; err != nil {
if err = h.DB.Preload("Host").Limit(30).Order("created_at desc").Find(logs).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.Render(http.StatusOK, "listlogs", echo.Map{
"logs": logs,
"logs": logs,
"title": h.Title,
})
}
@@ -45,11 +48,19 @@ func (h *Handler) ShowHostLogs(c echo.Context) (err error) {
}
logs := new([]model.Log)
if err = h.DB.Preload("Host").Where(&model.Log{HostID: uint(id)}).Limit(30).Find(logs).Error; err != nil {
if err = h.DB.Preload("Host").Where(&model.Log{HostID: uint(id)}).Order("created_at desc").Limit(30).Find(logs).Error; err != nil {
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
}
return c.Render(http.StatusOK, "listlogs", echo.Map{
"logs": logs,
"logs": logs,
"title": h.Title,
})
}
func (h *Handler) ClearLogs() {
var clearInterval = strconv.FormatUint(h.ClearInterval, 10) + " days"
h.DB.Exec("DELETE FROM LOGS WHERE created_at < datetime('now', '" + clearInterval + "');REINDEX LOGS;")
h.LastClearedLogs = time.Now()
log.Print("logs cleared")
}
+60 -25
View File
@@ -1,18 +1,21 @@
package main
import (
"net/http"
"github.com/benjaminbear/docker-ddns-server/dyndns/handler"
"github.com/foolin/goview"
"github.com/foolin/goview/supports/echoview-v4"
"github.com/go-playground/validator/v10"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
"html/template"
"net/http"
"time"
)
func main() {
// Set new instance
e := echo.New()
e.Logger.SetLevel(log.ERROR)
@@ -20,7 +23,17 @@ func main() {
e.Use(middleware.Logger())
// Set Renderer
e.Renderer = echoview.Default()
e.Renderer = echoview.New(goview.Config{
Root: "views",
Master: "layouts/master",
Extension: ".html",
Funcs: template.FuncMap{
"year": func() string {
return time.Now().Format("2006")
},
},
DisableCache: true,
})
// Set Validator
e.Validator = &handler.CustomValidator{Validator: validator.New()}
@@ -37,38 +50,60 @@ func main() {
}
defer h.DB.Close()
if err := h.ParseEnvs(); err != nil {
authAdmin, err := h.ParseEnvs()
if err != nil {
e.Logger.Fatal(err)
}
e.Use(middleware.BasicAuth(h.Authenticate))
// UI Routes
e.GET("/", func(c echo.Context) error {
//render with master
return c.Render(http.StatusOK, "listhosts", nil)
groupPublic := e.Group("/")
groupPublic.GET("*", func(c echo.Context) error {
//redirect to admin
return c.Redirect(301, "./admin/")
})
groupAdmin := e.Group("/admin")
if authAdmin {
groupAdmin.Use(middleware.BasicAuth(h.AuthenticateAdmin))
}
e.GET("/hosts/add", h.AddHost)
e.GET("/hosts/edit/:id", h.EditHost)
e.GET("/hosts", h.ListHosts)
e.GET("/cnames/add", h.AddCName)
e.GET("/cnames", h.ListCNames)
e.GET("/logs", h.ShowLogs)
e.GET("/logs/host/:id", h.ShowHostLogs)
groupAdmin.GET("/", h.ListHosts)
groupAdmin.GET("/hosts/add", h.AddHost)
groupAdmin.GET("/hosts/edit/:id", h.EditHost)
groupAdmin.GET("/hosts", h.ListHosts)
groupAdmin.GET("/cnames/add", h.AddCName)
groupAdmin.GET("/cnames", h.ListCNames)
groupAdmin.GET("/logs", h.ShowLogs)
groupAdmin.GET("/logs/host/:id", h.ShowHostLogs)
// Rest Routes
e.POST("/hosts/add", h.CreateHost)
e.POST("/hosts/edit/:id", h.UpdateHost)
e.GET("/hosts/delete/:id", h.DeleteHost)
e.POST("/cnames/add", h.CreateCName)
e.GET("/cnames/delete/:id", h.DeleteCName)
groupAdmin.POST("/hosts/add", h.CreateHost)
groupAdmin.POST("/hosts/edit/:id", h.UpdateHost)
groupAdmin.GET("/hosts/delete/:id", h.DeleteHost)
groupAdmin.POST("/cnames/add", h.CreateCName)
groupAdmin.GET("/cnames/delete/:id", h.DeleteCName)
// dyndns compatible api
e.GET("/update", h.UpdateIP)
e.GET("/nic/update", h.UpdateIP)
e.GET("/v2/update", h.UpdateIP)
e.GET("/v3/update", h.UpdateIP)
// (avoid breaking changes and create groups for each update endpoint)
updateRoute := e.Group("/update")
updateRoute.Use(middleware.BasicAuth(h.AuthenticateUpdate))
updateRoute.GET("", h.UpdateIP)
nicRoute := e.Group("/nic")
nicRoute.Use(middleware.BasicAuth(h.AuthenticateUpdate))
updateRoute.GET("/update", h.UpdateIP)
v2Route := e.Group("/v2")
v2Route.Use(middleware.BasicAuth(h.AuthenticateUpdate))
v2Route.GET("/update", h.UpdateIP)
v3Route := e.Group("/v3")
v3Route.Use(middleware.BasicAuth(h.AuthenticateUpdate))
v3Route.GET("/update", h.UpdateIP)
// health-check
e.GET("/ping", func(c echo.Context) error {
u := &handler.Error{
Message: "OK",
}
return c.JSON(http.StatusOK, u)
})
// Start server
e.Logger.Fatal(e.Start(":8080"))
+2 -2
View File
@@ -3,7 +3,7 @@ package nswrapper
import (
"bytes"
"errors"
"fmt"
"github.com/labstack/gommon/log"
"net"
"net/http"
"strings"
@@ -25,7 +25,7 @@ func GetIPType(ipAddr string) string {
// GetCallerIP searches for the "real" IP senders has actually.
// If its a private address we won't use it.
func GetCallerIP(r *http.Request) (string, error) {
fmt.Println("request", r.Header)
log.Info("request", r.Header)
for _, h := range []string{"X-Real-Ip", "X-Forwarded-For"} {
addresses := strings.Split(r.Header.Get(h), ",")
// march from right to left until we get a public address
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"fmt"
"github.com/labstack/gommon/log"
"io/ioutil"
"os"
"os/exec"
@@ -11,7 +12,7 @@ import (
// UpdateRecord builds a nsupdate file and updates a record by executing it with nsupdate.
func UpdateRecord(hostname string, target string, addrType string, zone string, ttl int) error {
fmt.Printf("%s record update request: %s -> %s\n", addrType, hostname, target)
log.Info(fmt.Sprintf("%s record update request: %s -> %s", addrType, hostname, target))
f, err := ioutil.TempFile(os.TempDir(), "dyndns")
if err != nil {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+21 -11
View File
@@ -1,18 +1,18 @@
$("button.addHost").click(function () {
location.href='/hosts/add';
location.href='/admin/hosts/add';
});
$("button.editHost").click(function () {
location.href='/hosts/edit/' + $(this).attr('id');
location.href='/admin/hosts/edit/' + $(this).attr('id');
});
$("button.deleteHost").click(function () {
$.ajax({
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
type: 'GET',
url: "/hosts/delete/" + $(this).attr('id')
url: "/admin/hosts/delete/" + $(this).attr('id')
}).done(function(data, textStatus, jqXHR) {
location.href="/hosts";
location.href="/admin/hosts";
}).fail(function(jqXHR, textStatus, errorThrown) {
alert("Error: " + $.parseJSON(jqXHR.responseText).message);
location.reload()
@@ -20,7 +20,7 @@ $("button.deleteHost").click(function () {
});
$("button.showHostLog").click(function () {
location.href='/logs/host/' + $(this).attr('id');
location.href='/admin/logs/host/' + $(this).attr('id');
});
$("button.add, button.edit").click(function () {
@@ -53,9 +53,9 @@ $("button.add, button.edit").click(function () {
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
data: $('#editHostForm').serialize(),
type: 'POST',
url: '/'+type+'/'+action+id,
url: '/admin/'+type+'/'+action+id,
}).done(function(data, textStatus, jqXHR) {
location.href="/"+type;
location.href="/admin/"+type;
}).fail(function(jqXHR, textStatus, errorThrown) {
alert("Error: " + $.parseJSON(jqXHR.responseText).message);
});
@@ -89,16 +89,16 @@ $("#logout").click(function (){
});
$("button.addCName").click(function () {
location.href='/cnames/add';
location.href='/admin/cnames/add';
});
$("button.deleteCName").click(function () {
$.ajax({
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
type: 'GET',
url: "/cnames/delete/" + $(this).attr('id')
url: "/admin/cnames/delete/" + $(this).attr('id')
}).done(function(data, textStatus, jqXHR) {
location.href="/cnames";
location.href="/admin/cnames";
}).fail(function(jqXHR, textStatus, errorThrown) {
alert("Error: " + $.parseJSON(jqXHR.responseText).message);
location.reload()
@@ -124,6 +124,16 @@ $("button.copyToClipboard").click(function () {
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
});
$("button.copyUrlToClipboard").click(function () {
let id = $(this).attr('id');
let hostname = document.getElementById('host-hostname_'+id).innerHTML
let domain = document.getElementById('host-domain_'+id).innerHTML
let username = document.getElementById('host-username_'+id).innerHTML
let password = document.getElementById('host-password_'+id).innerHTML
let out = location.protocol + '//' +username.trim()+':'+password.trim()+'@'+ domain
out +='/update?hostname='+hostname
navigator.clipboard.writeText(out.trim());
});
function randomHash() {
let chars = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()-+<>ABCDEFGHIJKLMNOP1234567890";
@@ -155,7 +165,7 @@ $(document).ready(function(){
}
});
urlPath = new URL(window.location.href).pathname.split("/")[1];
urlPath = new URL(window.location.href).pathname.split("/")[2];
if (urlPath === "") {
urlPath = "hosts"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13 -12
View File
@@ -7,13 +7,13 @@
<meta name="author" content="">
<link rel="icon" href="/static/icons/favicon.ico">
<title>TheBBCloud DynDNS</title>
<title>{{.title}}</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<!-- JQueryUI base CSS -->
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.min.css" integrity="sha256-sEGfrwMkIjbgTBwGLVK38BG/XwIiNC/EAG9Rzsfda6A=" crossorigin="anonymous" />
<link rel="stylesheet" href="/static/css/jquery-ui.min.css"/>
<!-- Custom styles for this template -->
<link href="/static/css/narrow-jumbotron.css" rel="stylesheet">
@@ -27,27 +27,27 @@
<nav>
<ul class="nav nav-pills float-right">
<li class="nav-item">
<a class="nav-link nav-hosts" href="/hosts">Hosts</a>
<a class="nav-link nav-hosts" href="/admin/hosts">Hosts</a>
</li>
<li class="nav-item">
<a class="nav-link nav-cnames" href="/cnames">CNames</a>
<a class="nav-link nav-cnames" href="/admin/cnames">CNames</a>
</li>
<li class="nav-item">
<a class="nav-link nav-logs" href="/logs">Logs</a>
<a class="nav-link nav-logs" href="/admin/logs">Logs</a>
</li>
<li class="nav-item">
<a class="nav-link nav-logout" href="" id="logout">Logout</a>
</li>
</ul>
</nav>
<h3 class="text-muted">TheBBCloud DynDNS</h3>
<h3 class="text-muted">{{.title}}</h3>
</div>
<!-- Page Content -->
{{template "content" .}}
<footer class="footer">
<p>&copy; TheBBCloud 2021</p>
<p>&copy; {{.title}} {{year}}</p>
</footer>
</div> <!-- /container -->
@@ -56,10 +56,11 @@
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script>
<script src="/static/js/jquery-3.4.1.min.js"></script>
<!-- popper.js@1.16.0 -->
<script src="/static/js/popper.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/jquery-ui.min.js"></script>
<script src="/static/js/actions-1.0.0.js"></script>
</body>
</html>
+52 -25
View File
@@ -1,27 +1,54 @@
{{define "content"}}
<div class="container marketing">
<h3 class="text-center mb-4">DNS Host Entries</h3>
<table class="table table-striped text-center">
<thead>
<tr>
<th>Hostname</th>
<th>IP</th>
<th>TTL</th>
<th>LastUpdate</th>
<th><button class="addHost btn btn-primary">Add Host Entry</button></th>
</tr>
</thead>
<tbody>
{{range .hosts}}
<tr>
<td>{{.Hostname}}.{{.Domain}}</td>
<td>{{.Ip}}</td>
<td>{{.Ttl}}</td>
<td>{{.LastUpdate.Format "01/02/2006 15:04 MEZ"}}</td>
<td><button id="{{.ID}}" class="editHost btn btn-outline-secondary btn-sm"><img src="/static/icons/pencil.svg" alt="" width="16" height="16" title="Edit"></button>&nbsp;<button id="{{.ID}}" class="deleteHost btn btn-outline-secondary btn-sm"><img src="/static/icons/trash.svg" alt="" width="16" height="16" title="Delete"></button> <button id="{{.ID}}" class="showHostLog btn btn-outline-secondary btn-sm"><img src="/static/icons/table.svg" alt="" width="16" height="16" title="Logs"></button></td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="container marketing">
<h3 class="text-center mb-4">DNS Host Entries</h3>
<table class="table table-striped text-center">
<thead>
<tr>
<th>Domain</th>
<th>Hostname</th>
<th>IP</th>
<th>TTL</th>
<th>LastUpdate</th>
<th>
<button class="addHost btn btn-primary">Add Host Entry</button>
</th>
</tr>
</thead>
<tbody>
{{range .hosts}}
<tr id="host_{{.ID}}">
<td id="host-domain_{{.ID}}">{{.Domain}}</td>
<td id="host-hostname_{{.ID}}">{{.Hostname}}.{{.Domain}}</td>
<td>{{.Ip}}</td>
<td>{{.Ttl}}</td>
<td>{{.LastUpdate.Format "01/02/2006 15:04 MEZ"}}</td>
<td>
<div style="display:none">
<div id="host-username_{{.ID}}">
{{.UserName}}
</div>
<div id="host-password_{{.ID}}" >
{{.Password}}
</div>
</div>
<div class="btn-group" id="host_{{.ID}}" >
<button id="{{.ID}}" class="editHost btn btn-outline-secondary btn-sm"><img
src="/static/icons/pencil.svg" alt="" width="16" height="16" title="Edit"></button>&nbsp;
<button
id="{{.ID}}" class="deleteHost btn btn-outline-secondary btn-sm"><img
src="/static/icons/trash.svg"
alt="" width="16" height="16"
title="Delete"></button> &nbsp;
<button id="{{.ID}}" class="showHostLog btn btn-outline-secondary btn-sm"><img
src="/static/icons/table.svg" alt="" width="16" height="16" title="Logs"></button> &nbsp;
<button id="{{.ID}}" class="copyUrlToClipboard btn btn-outline-secondary btn-sm"><img
src="/static/icons/clipboard.svg" alt="" width="16" height="16" title="Copy URL to clipboard"></button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}