feat: add secure Pi-hole DNS management
Container-Image veröffentlichen / container (push) Has been cancelled
Container-Image veröffentlichen / container (push) Has been cancelled
This commit is contained in:
1 parent
1a6b4d0939
commit
1c32ca0671
18 files changed
+2159
-34
No files matched your search
@@ -41,12 +41,10 @@
|
||||
<div class="danger-actions"><form action="/targets/{{.ID}}/archive" method="post" class="unlink-form">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<strong>Verknüpfung trennen</strong>
|
||||
<p>Entfernt nur die lokale DynDNS-Verknüpfung. Die DNS-Records bleiben erhalten.</p>
|
||||
<button class="button button-danger-soft" type="submit">Verknüpfung trennen</button>
|
||||
</form><form action="/targets/{{.ID}}/delete" method="post" class="unlink-form">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<strong>Ziel vollständig löschen</strong>
|
||||
<p>Löscht das Ziel und alle zugehörigen A-/AAAA-Records bei Hetzner.</p>
|
||||
<button class="button button-danger-soft" type="submit">Ziel löschen</button>
|
||||
</form></div>
|
||||
{{end}}
|
||||
@@ -79,12 +77,10 @@
|
||||
<div class="danger-actions"><form action="/cnames/{{.ID}}/remove" method="post" class="unlink-form">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<strong>Aus WebUI entfernen</strong>
|
||||
<p>Entfernt nur die lokale Verknüpfung. Der CNAME-Record bei Hetzner bleibt unverändert.</p>
|
||||
<button class="button button-danger-soft" type="submit">Aus WebUI entfernen</button>
|
||||
</form><form action="/cnames/{{.ID}}/delete" method="post" class="unlink-form">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<strong>Ziel vollständig löschen</strong>
|
||||
<p>Löscht das Ziel aus der WebUI und den zugehörigen CNAME-Record bei Hetzner.</p>
|
||||
<button class="button button-danger-soft" type="submit">Ziel löschen</button>
|
||||
</form></div>
|
||||
</div></details>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "errors"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<header class="page-heading">
|
||||
<div><a class="back-link" href="/">← Übersicht</a><span class="page-kicker">Diagnose</span><h1>Fehlerzentrale</h1><p>Aktuelle Ziel- und Zonenfehler sowie alle gespeicherten fehlgeschlagenen oder partiellen Update-Läufe.</p></div>
|
||||
<div><a class="back-link" href="/">← Übersicht</a><span class="page-kicker">Diagnose</span><h1>Fehlerzentrale</h1><p>Aktuelle DynDNS-, Zonen- und Pi-hole-Probleme sowie fehlgeschlagene oder partielle Update-Läufe.</p></div>
|
||||
<form action="/errors/clear" method="post">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<button class="button button-danger-soft" type="submit" {{if eq .ErrorCount 0}}disabled{{end}}>Fehlerliste leeren</button>
|
||||
@@ -9,11 +9,12 @@
|
||||
</header>
|
||||
|
||||
{{if eq .ErrorCount 0}}
|
||||
<section class="panel error-empty"><span class="error-empty-icon" aria-hidden="true">✓</span><div><h2>Keine Fehler gespeichert</h2><p>Aktuell sind weder Ziel- oder Zonenfehler noch problematische Update-Läufe vorhanden.</p></div></section>
|
||||
<section class="panel error-empty"><span class="error-empty-icon" aria-hidden="true">✓</span><div><h2>Keine Fehler gespeichert</h2><p>Aktuell sind weder DNS- oder Pi-hole-Fehler noch problematische Update-Läufe vorhanden.</p></div></section>
|
||||
{{else}}
|
||||
<section class="error-summary-grid" aria-label="Fehlerübersicht">
|
||||
<article><span>Ziele</span><strong>{{len .ErrorTargets}}</strong></article>
|
||||
<article><span>Zonen</span><strong>{{len .ErrorZones}}</strong></article>
|
||||
<article><span>Pi-hole</span><strong>{{len .ErrorPiHoles}}</strong></article>
|
||||
<article><span>Update-Läufe</span><strong>{{len .ErrorRuns}}</strong></article>
|
||||
</section>
|
||||
|
||||
@@ -28,6 +29,17 @@
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .ErrorPiHoles}}
|
||||
<section class="panel error-section">
|
||||
<div class="panel-heading"><div><span class="section-label">Lokales Netzwerk</span><h2>Pi-hole-Probleme</h2></div><span class="status-pill status-bad">{{len .ErrorPiHoles}}</span></div>
|
||||
<div class="error-list">
|
||||
{{range .ErrorPiHoles}}
|
||||
<article class="error-item"><span class="error-mark" aria-hidden="true">!</span><div><strong>{{.Name}}</strong><p>{{.LastError}}</p><small>{{.BaseURL}} · letzte Prüfung {{formatTime .LastSyncAt}}</small></div><a href="/pihole#local-dns">Öffnen →</a></article>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .ErrorZones}}
|
||||
<section class="panel error-section">
|
||||
<div class="panel-heading"><div><span class="section-label">Provider</span><h2>Zonenprobleme</h2></div><span class="status-pill status-bad">{{len .ErrorZones}}</span></div>
|
||||
|
||||
@@ -5,29 +5,30 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>{{.Title}} · Hetzner DynDNS</title>
|
||||
<link rel="stylesheet" href="/static/styles.css?v=features-5">
|
||||
<script src="/static/app.js?v=features-5" defer></script>
|
||||
<title>{{.Title}} · DNS Control</title>
|
||||
<link rel="stylesheet" href="/static/styles.css?v=features-12">
|
||||
<script src="/static/app.js?v=features-12" defer></script>
|
||||
</head>
|
||||
<body class="page-{{.Page}}">
|
||||
<a class="skip-link" href="#main">Zum Inhalt springen</a>
|
||||
{{if and (ne .Page "setup") (ne .Page "login")}}
|
||||
<div class="admin-layout">
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="/" aria-label="Hetzner DynDNS Startseite">
|
||||
<a class="brand" href="/" aria-label="DNS Control Startseite">
|
||||
<span class="brand-mark">DD</span>
|
||||
<span><strong>DynDNS Console</strong><small>Hetzner DNS</small></span>
|
||||
<span><strong>DNS Control</strong><small>Hetzner + Pi-hole</small></span>
|
||||
</a>
|
||||
<div class="instance-state {{if not .UpdaterEnabled}}is-inactive{{end}}"><span></span> Dienst {{if .UpdaterEnabled}}aktiv{{else}}inaktiv{{end}}</div>
|
||||
<nav aria-label="Hauptnavigation">
|
||||
<a href="/" {{if eq .Page "dashboard"}}aria-current="page"{{end}}><span class="nav-icon">01</span>Übersicht</a>
|
||||
<a href="/domains" {{if eq .Page "domains"}}aria-current="page"{{end}}><span class="nav-icon">02</span>Domains</a>
|
||||
{{if .IsAdmin}}
|
||||
<a href="/settings" {{if eq .Page "settings"}}aria-current="page"{{end}}><span class="nav-icon">03</span>Updater</a>
|
||||
<a href="/errors" {{if eq .Page "errors"}}aria-current="page"{{end}}><span class="nav-icon">04</span>Fehler</a>
|
||||
<a href="/users" {{if eq .Page "users"}}aria-current="page"{{end}}><span class="nav-icon">05</span>Benutzer</a>
|
||||
<a href="/pihole" {{if eq .Page "pihole"}}aria-current="page"{{end}}><span class="nav-icon">03</span>Pi-hole</a>
|
||||
<a href="/settings" {{if eq .Page "settings"}}aria-current="page"{{end}}><span class="nav-icon">04</span>Updater</a>
|
||||
<a href="/errors" {{if eq .Page "errors"}}aria-current="page"{{end}}><span class="nav-icon">05</span>Fehler</a>
|
||||
<a href="/users" {{if eq .Page "users"}}aria-current="page"{{end}}><span class="nav-icon">06</span>Benutzer</a>
|
||||
{{end}}
|
||||
<a href="/security" {{if eq .Page "security"}}aria-current="page"{{end}}><span class="nav-icon">{{if .IsAdmin}}06{{else}}03{{end}}</span>Sicherheit</a>
|
||||
<a href="/security" {{if eq .Page "security"}}aria-current="page"{{end}}><span class="nav-icon">{{if .IsAdmin}}07{{else}}03{{end}}</span>Sicherheit</a>
|
||||
</nav>
|
||||
<div class="sidebar-account">
|
||||
<span class="account-avatar">{{initial .User.DisplayName}}</span>
|
||||
@@ -40,7 +41,7 @@
|
||||
</aside>
|
||||
<div class="workspace">
|
||||
<header class="workspace-bar">
|
||||
<div><span>DynDNS</span><strong>/ {{.Title}}</strong></div>
|
||||
<div><span>DNS Control</span><strong>/ {{.Title}}</strong></div>
|
||||
{{if eq .Mode "lan"}}<span class="environment-badge">LAN · HTTP</span>{{else}}<span class="environment-badge secure">HTTPS</span>{{end}}
|
||||
</header>
|
||||
<main id="main" class="app-shell">
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{{define "pihole"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<header class="page-heading">
|
||||
<div><span class="page-kicker pihole-kicker">Lokales Netzwerk</span><h1>Pi-hole DNS</h1><p>Lokale A- und AAAA-Auflösungen ähnlich wie deine öffentlichen Domains verwalten.</p></div>
|
||||
{{if .PiHoleView}}
|
||||
<form action="/pihole/{{.PiHoleView.Integration.ID}}/refresh" method="post"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="button button-quiet" type="submit">Records neu einlesen</button></form>
|
||||
{{else}}
|
||||
<a class="button button-pihole" href="#pihole-connect">Pi-hole verbinden</a>
|
||||
{{end}}
|
||||
</header>
|
||||
|
||||
{{if .PiHoleView}}
|
||||
{{$view := .PiHoleView}}
|
||||
{{$integration := $view.Integration}}
|
||||
<section class="zone-summary pihole-summary" aria-label="Pi-hole-Status">
|
||||
<div><span>Provider</span><strong>Pi-hole API v6</strong></div>
|
||||
<div><span>Letzter Import</span><strong>{{formatTime $integration.LastSyncAt}}</strong></div>
|
||||
<div><span>Verknüpfte Einträge</span><strong>{{len $view.Records}}</strong></div>
|
||||
<div><span>Getrennte Einträge</span><strong>{{len $view.ArchivedRecords}}</strong></div>
|
||||
</section>
|
||||
|
||||
{{if $view.FetchError}}
|
||||
<div class="pihole-connection-error"><strong>Pi-hole konnte nicht neu eingelesen werden</strong><p>{{$view.FetchError}}</p></div>
|
||||
{{end}}
|
||||
|
||||
<section class="targets-section" id="local-dns">
|
||||
<div class="section-heading"><div><span class="section-label pihole-kicker">Verknüpfte Records</span><h2>Lokale DNS-Einträge</h2></div><span class="record-count">{{len $view.Records}} Einträge</span></div>
|
||||
{{if $view.Records}}
|
||||
<div class="target-list">
|
||||
{{range $view.Records}}
|
||||
<article class="target-row pihole-target-row {{if .Active}}is-active{{end}}">
|
||||
<div class="target-main"><span class="target-status {{if .Active}}pihole-on{{end}}" aria-label="{{if .Active}}aktiv{{else}}inaktiv{{end}}"></span><div><h3>{{.Hostnames}}</h3><p><span class="family-chip">{{if contains .IP ":"}}AAAA{{else}}A{{end}}</span><span>{{.IP}}</span></p></div></div>
|
||||
<div class="target-meta"><span>Pi-hole DNS</span><span class="status-pill {{if .Active}}pihole-status-pill{{end}}">{{if .Active}}Aktiv{{else}}Inaktiv{{end}}</span></div>
|
||||
<details class="target-edit"><summary>Einstellungen <span>⌄</span></summary><div class="edit-inner">
|
||||
{{if .Valid}}
|
||||
<form action="/pihole/{{$integration.ID}}/records/update" method="post" class="target-form pihole-record-form" data-dirty-form>
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="link_id" value="{{.LinkID}}">
|
||||
<div class="toggle-row"><label class="switch pihole-switch"><input type="checkbox" name="active" {{if .Active}}checked{{end}}><span></span> Eintrag aktiv</label></div>
|
||||
<div class="form-grid"><label>IP-Adresse<input name="ip" value="{{.IP}}" inputmode="text" required></label><label>Hostname(n)<input name="hostnames" value="{{.Hostnames}}" required></label></div>
|
||||
<button class="button button-pihole button-compact pihole-record-save" type="submit" data-dirty-submit disabled aria-disabled="true">Speichern</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<p class="inline-error">Der vorhandene Pi-hole-Wert kann nicht strukturiert bearbeitet werden: <code>{{.Raw}}</code></p>
|
||||
{{end}}
|
||||
<div class="danger-actions">
|
||||
<form action="/pihole/{{$integration.ID}}/records/archive" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="link_id" value="{{.LinkID}}"><strong>Nur aus der WebUI entfernen</strong><button class="button button-danger-soft" type="submit">Aus WebUI entfernen</button></form>
|
||||
<form action="/pihole/{{$integration.ID}}/records/delete" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="link_id" value="{{.LinkID}}"><strong>Eintrag vollständig löschen</strong><button class="button button-danger-soft" type="submit">Eintrag löschen</button></form>
|
||||
</div>
|
||||
</div></details>
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}<div class="empty-state">Keine verknüpften lokalen DNS-Einträge vorhanden. Lege unten einen neuen Eintrag an.</div>{{end}}
|
||||
</section>
|
||||
|
||||
{{if $view.ArchivedRecords}}
|
||||
<section class="panel archive-panel">
|
||||
<div class="panel-heading"><div><span class="section-label pihole-kicker">Archiv</span><h2>Aus der WebUI entfernte Einträge</h2></div><span class="record-count">{{len $view.ArchivedRecords}} Einträge</span></div>
|
||||
<p class="panel-description">Diese lokalen DNS-Einträge bestehen weiterhin bei Pi-hole, sind aber nicht mehr mit der aktiven WebUI-Liste verknüpft.</p>
|
||||
<div class="archive-list">
|
||||
{{range $view.ArchivedRecords}}
|
||||
<div class="archive-row"><span><strong>{{.Hostnames}}</strong><small>{{if contains .IP ":"}}AAAA{{else}}A{{end}} · {{.IP}} · {{if .Active}}aktiv{{else}}inaktiv{{end}} · getrennt {{formatTime .ArchivedAt}}</small></span><form action="/pihole/{{$integration.ID}}/records/restore" method="post"><input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="link_id" value="{{.LinkID}}"><button class="button button-quiet" type="submit">Wieder verknüpfen</button></form></div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<section class="action-grid admin-actions-grid pihole-action-grid">
|
||||
<article class="panel"><div class="panel-heading"><div><span class="section-label pihole-kicker">Record anlegen</span><h2>Neuer lokaler DNS-Eintrag</h2></div></div>
|
||||
<form action="/pihole/{{$integration.ID}}/records" method="post" class="stacked-form pihole-record-form pihole-create-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><div class="toggle-row"><label class="switch pihole-switch"><input type="checkbox" name="active" checked><span></span> Eintrag aktivieren</label></div><div class="form-grid"><label>Hostname(n)<input name="hostnames" placeholder="nas.lan" required></label><label>IP-Adresse<input name="ip" inputmode="text" placeholder="192.168.1.20 oder fd00::20" required></label></div><button class="button button-pihole button-compact pihole-record-save" type="submit">Record anlegen</button></form>
|
||||
</article>
|
||||
<article class="panel" id="pihole-connection"><div class="panel-heading"><div><span class="section-label pihole-kicker">API-Zugang</span><h2>Verbindung verwalten</h2></div><span class="status-pill {{if $view.FetchError}}status-bad{{else}}pihole-status-pill{{end}}">{{if $view.FetchError}}Nicht erreichbar{{else}}Verbunden{{end}}</span></div>
|
||||
<p class="panel-description">Das Anwendungspasswort bleibt verschlüsselt und standardmäßig verborgen. Eine Anzeige erfordert erneut dein Admin-Passwort.</p>
|
||||
<form action="/pihole/{{$integration.ID}}/save" method="post" class="stacked-form" data-dirty-form><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Pi-hole-Adresse <input name="base_url" type="url" value="{{$integration.BaseURL}}" required><small>Basis-URL oder vollständiger <code>/api</code>-Pfad.</small></label><label>Neues Anwendungspasswort <input name="password" type="password" autocomplete="new-password"><small>Leer lassen, um das gespeicherte Passwort beizubehalten.</small></label><label class="switch pihole-switch"><input type="checkbox" name="insecure_tls" {{if $integration.InsecureTLS}}checked{{end}}><span></span> TLS-Zertifikat nicht prüfen</label><button class="button button-pihole" type="submit" data-dirty-submit disabled aria-disabled="true">Verbindung prüfen und speichern</button></form>
|
||||
<div class="panel-divider"></div>
|
||||
<details><summary class="button button-quiet">Anwendungspasswort anzeigen</summary><form action="/pihole/{{$integration.ID}}/password" method="post" class="stacked-form inset-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><label>Admin-Passwort <input type="password" name="password" autocomplete="current-password" required></label><button class="button button-danger-soft" type="submit">Passwort entschlüsseln</button></form></details>
|
||||
<div class="panel-divider"></div>
|
||||
<details><summary class="text-danger">Pi-hole-Verbindung entfernen</summary><div class="danger-actions inset-form"><form action="/pihole/{{$integration.ID}}/remove" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><strong>Nur aus der WebUI entfernen</strong><p>Löscht ausschließlich Zugang und lokale Verknüpfungen. Alle DNS-Einträge bei Pi-hole bleiben bestehen.</p><button class="button button-danger-soft" type="submit">Lokale Verbindung entfernen</button></form><form action="/pihole/{{$integration.ID}}/delete" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><strong>Pi-hole vollständig leeren</strong><p>Löscht alle lokalen DNS-Einträge bei Pi-hole und entfernt anschließend die Verbindung aus der WebUI.</p><button class="button button-danger-soft" type="submit">Verbindung und Einträge löschen</button></form></div></details>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{{else}}
|
||||
<section class="empty-board pihole-empty"><span class="empty-icon pihole-empty-icon">PH</span><h2>Noch kein Pi-hole verbunden</h2><p>Verbinde genau einen Pi-hole über die v6-API. Danach lassen sich lokale DNS-Einträge wie Records im Domain-Tab verwalten.</p><a class="button button-pihole" href="#pihole-connect">Pi-hole verbinden</a></section>
|
||||
<section class="form-panel pihole-connect-panel" id="pihole-connect">
|
||||
<div class="panel-heading"><div><span class="section-label pihole-kicker">API-Verbindung</span><h2>Pi-hole verbinden</h2></div><span class="pihole-api-badge">API v6</span></div>
|
||||
<p class="panel-description">Verwende ein eigenes Pi-hole-Anwendungspasswort. Es wird mit dem Master-Key dieser Anwendung verschlüsselt.</p>
|
||||
<form action="/pihole/connect" method="post" class="stacked-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Pi-hole-Adresse <input name="base_url" type="url" placeholder="https://pi.hole" required><small><code>/api</code> wird automatisch ergänzt.</small></label><label>Anwendungspasswort <input name="password" type="password" autocomplete="new-password" required><small>Keinen Session-Token eintragen.</small></label><label class="switch pihole-switch"><input type="checkbox" name="insecure_tls"><span></span> TLS-Zertifikat nicht prüfen</label><div class="warning-box">Nur für ein selbstsigniertes Zertifikat in einem vertrauenswürdigen internen Netz verwenden. Bei HTTP wird das Passwort auf dem Netzwerkweg unverschlüsselt übertragen.</div><button class="button button-pihole" type="submit">Verbindung testen und speichern</button></form>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{define "token"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<section class="token-card"><span class="page-kicker">Sensible Zugangsdaten</span><h1>API-Token</h1><p>Zone: <strong>{{.Zone.Name}}</strong>. Der Wert wird nicht in URL, Audit-Log oder Browser-Cache gespeichert.</p><div class="token-value"><code id="api-token">{{.Token}}</code><button class="button button-primary" type="button" data-copy-target="api-token">Kopieren</button></div><p class="warning-box">Nach dem Kopieren diese Seite schließen. Eine erneute Anzeige erfordert wieder das Admin-Passwort.</p><a class="button button-quiet" href="/domains/{{.Zone.ID}}">Zur Domain zurückkehren</a></section>
|
||||
<section class="token-card"><span class="page-kicker">Sensible Zugangsdaten</span><h1>{{.SecretTitle}}</h1><p><strong>{{.SecretScope}}</strong>. Der Wert wird nicht in URL, Audit-Log oder Browser-Cache gespeichert.</p><div class="token-value"><code id="secret-value">{{.SecretValue}}</code><button class="button button-primary" type="button" data-copy-target="secret-value">Kopieren</button></div><p class="warning-box">Nach dem Kopieren diese Seite schließen. Eine erneute Anzeige erfordert wieder das Admin-Passwort.</p><a class="button button-quiet" href="{{.SecretBackURL}}">Zurückkehren</a></section>
|
||||
{{end}}
|
||||
Reference in new issue
Block a user