Add DNS accounts, Let's Encrypt, password change, reboot, HTTP redirect
- router.dnsProviders: named accounts at DNS providers, used by dynamic DNS and certificates (ddclient and lego provider lists in pkgs/router-webui/providers.json). Schema 2 migrates router.ddns. - router.acme: a Let's Encrypt certificate for the web UI via security.acme (DNS-01, resolver 1.1.1.1), served and reloaded by the web UI; Certificate page. - Web UI: password change (web UI and root), reboot button, HTTP to HTTPS redirect (router.webui.httpPort), grouped menu. - Latest kernel (linuxPackages_latest); 7.3 and 7.3.0 are the same kernel for the reboot hint. - SPECS.md: wanted later (REST API, GraphQL, MCP). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -21,6 +21,24 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Password</h2>
|
||||
<p>One password for this web UI and for root on the console and over SSH.</p>
|
||||
<form method="post" action="/password" class="narrow">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<label>Current password <input type="password" name="current" autocomplete="current-password" required></label>
|
||||
<label>New password (at least 8 characters) <input type="password" name="new" autocomplete="new-password" minlength="8" required></label>
|
||||
<label>New password again <input type="password" name="confirm" autocomplete="new-password" minlength="8" required></label>
|
||||
<button>Change password</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Reboot</h2>
|
||||
<p>Restarts the router; routing stops for a minute or two. A new kernel from an update only runs after this.</p>
|
||||
<form method="post" action="/reboot" data-confirm="Reboot the router now?"><input type="hidden" name="csrf" value="{{.CSRF}}"><button>Reboot</button></form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Backup</h2>
|
||||
<p>Everything on <code>/var/lib/router</code>: settings, PPPoE password, WireGuard keys, passwords, the web UI's
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{{define "content"}}
|
||||
{{with .Data}}
|
||||
{{$e := .Errors}}{{$f := .Form}}
|
||||
<section>
|
||||
<h2>Serving now</h2>
|
||||
{{if .Has}}
|
||||
<dl>
|
||||
<dt>Certificate for</dt><dd>{{.Cert.Subject}}</dd>
|
||||
<dt>Issued by</dt><dd>{{.Cert.Issuer}}{{if .Cert.SelfSigned}} <span class="bad">self-signed: browsers warn</span>{{end}}</dd>
|
||||
<dt>Valid until</dt><dd>{{.Cert.NotAfter.Format "2006-01-02"}}</dd>
|
||||
<dt>SHA-256</dt><dd><code>{{.Cert.Fingerprint}}</code></dd>
|
||||
</dl>
|
||||
{{else}}<p>No certificate yet; the web UI creates a self-signed one when it starts.</p>{{end}}
|
||||
</section>
|
||||
<form method="post" action="/certificate">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<fieldset>
|
||||
<legend>Let's Encrypt</legend>
|
||||
<label class="inline"><input type="checkbox" name="enabled" {{if $f.Enabled}}checked{{end}}> Get a certificate for the web UI and renew it</label>
|
||||
<div>
|
||||
<label>DNS account <select name="provider"><option value=""></option>
|
||||
{{range .Providers}}<option value="{{.Name}}" {{if eq .Name $f.Provider}}selected{{end}}>{{.Name}} ({{.Type}})</option>{{end}}
|
||||
</select></label>
|
||||
<label>Contact e-mail <input name="email" value="{{$f.Email}}" size="26" spellcheck="false" placeholder="you@example.org"></label>
|
||||
</div>
|
||||
{{range $k := list "provider" "email"}}{{with index $e $k}}<p class="field-error">{{$k}}: {{.}}</p>{{end}}{{end}}
|
||||
<p class="hint">The account must be able to change the DNS records of the names: Let's Encrypt is shown it by a TXT record, so no port
|
||||
needs to be open to the internet. Accounts are kept on the <a href="/dnsproviders">DNS providers</a> page (cloudflare, desec,
|
||||
digitalocean, duckdns and hetzner work here).{{if not .Providers}} <strong>There is none yet.</strong>{{end}}</p>
|
||||
<label>Names, one per line; the first is the certificate's name <textarea name="domains" rows="4" cols="40" spellcheck="false">{{$f.Domains}}</textarea></label>
|
||||
{{with index $e "domains"}}<p class="field-error">{{.}}</p>{{end}}
|
||||
<p class="hint">A wildcard like <code>*.home.example.org</code> works. The names should lead to the router (see DNS) so the browser reaches it by name.</p>
|
||||
<label class="inline"><input type="checkbox" name="staging" {{if $f.Staging}}checked{{end}}> Test first with the staging service (its certificates are not trusted by browsers); uncheck for the real one</label>
|
||||
<label class="inline"><input type="checkbox" name="agree" {{if $f.Agree}}checked{{end}}> I accept the <a href="https://letsencrypt.org/repository/">Let's Encrypt subscriber agreement</a></label>
|
||||
{{with index $e "agree"}}<p class="field-error">{{.}}</p>{{end}}
|
||||
</fieldset>
|
||||
<button>Save</button>
|
||||
</form>
|
||||
<section>
|
||||
<h2>Ordering</h2>
|
||||
{{if .Status}}<p class="muted">{{.Status}}</p>{{if .Log}}<pre class="log">{{.Log}}</pre>{{end}}
|
||||
{{else}}<p class="muted">Nothing yet. After you save and apply, the router orders the certificate (a minute or two); renewals follow 30 days before it expires and need no restart.</p>{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -33,7 +33,7 @@
|
||||
<h2>System</h2>
|
||||
<dl>
|
||||
<dt>Uptime</dt><dd>{{age $sys.Uptime}}</dd>
|
||||
{{if $sys.Kernel}}<dt>Kernel</dt><dd>{{$sys.Kernel}}{{if $sys.KernelNext}} <span class="bad">reboot to run {{$sys.KernelNext}}</span>{{end}}</dd>{{end}}
|
||||
{{if $sys.Kernel}}<dt>Kernel</dt><dd>{{$sys.Kernel}}{{if $sys.KernelNext}} <span class="bad">reboot to run {{$sys.KernelNext}}</span> <a href="/advanced">Reboot</a>{{end}}</dd>{{end}}
|
||||
<dt>Load</dt><dd>{{$sys.Load}}</dd>
|
||||
<dt>CPU</dt><dd>{{value $sys.CPU "%"}}</dd>
|
||||
{{if $sys.MemTotal}}<dt>Memory</dt><dd>{{bytes $sys.MemUsed}} of {{bytes $sys.MemTotal}}</dd>{{end}}
|
||||
|
||||
@@ -10,20 +10,14 @@
|
||||
<legend>Provider</legend>
|
||||
<label class="inline"><input type="checkbox" name="enabled" {{if $f.Enabled}}checked{{end}}> Keep these names up to date</label>
|
||||
<div>
|
||||
<label>Provider (protocol) <input name="protocol" value="{{$f.Protocol}}" size="16" list="protocols" spellcheck="false"></label>
|
||||
<datalist id="protocols"><option value="cloudflare"><option value="duckdns"><option value="dyndns2"><option value="namecheap"><option value="porkbun"><option value="hetzner"><option value="godaddy"><option value="digitalocean"></datalist>
|
||||
<label>Server <input name="server" value="{{$f.Server}}" size="26" spellcheck="false" placeholder="only for dyndns2"></label>
|
||||
<label>Account <select name="provider"><option value=""></option>
|
||||
{{range .Providers}}<option value="{{.Name}}" {{if eq .Name $f.Provider}}selected{{end}}>{{.Name}} ({{.Type}})</option>{{end}}
|
||||
</select></label>
|
||||
<label>Zone <input name="zone" value="{{$f.Zone}}" size="22" spellcheck="false" placeholder="example.org (Cloudflare)"></label>
|
||||
</div>
|
||||
{{range $k := list "protocol" "server" "zone"}}{{with index $e $k}}<p class="field-error">{{$k}}: {{.}}</p>{{end}}{{end}}
|
||||
<div>
|
||||
<label>Login <input name="username" value="{{$f.Username}}" size="18" spellcheck="false" autocomplete="off"></label>
|
||||
<label>Password or API token <input type="password" name="secret" size="30" autocomplete="new-password"
|
||||
placeholder="{{if $f.SecretSet}}stored; leave empty to keep{{else}}required{{end}}"></label>
|
||||
</div>
|
||||
{{with index $e "secret"}}<p class="field-error">{{.}}</p>{{end}}
|
||||
<p class="hint">Login is <code>token</code> for a Cloudflare API token (permission Zone → DNS → Edit). The secret is kept on the router
|
||||
in a private file, not in the settings.</p>
|
||||
{{range $k := list "provider" "zone"}}{{with index $e $k}}<p class="field-error">{{$k}}: {{.}}</p>{{end}}{{end}}
|
||||
<p class="hint">The account (login and token) is kept on the <a href="/dnsproviders">DNS providers</a> page, where certificates use it too.
|
||||
{{if not .Providers}}<strong>There is none yet.</strong>{{end}}</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Names</legend>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{{define "content"}}
|
||||
{{with .Data}}
|
||||
{{$e := .Errors}}
|
||||
<p class="hint">Accounts at DNS providers, kept once and used by <a href="/ddns">Dynamic DNS</a> (the providers ddclient knows) and by the
|
||||
<a href="/certificate">certificate</a> (the providers lego knows). Secrets stay on the router in private files, not in the settings,
|
||||
and are never shown again.<br>
|
||||
<strong>Password or API token</strong>: for dynamic DNS, and for certificates through cloudflare, desec, digitalocean, duckdns and hetzner.<br>
|
||||
<strong>Certificate variables</strong>: for certificates through any other provider, the variables lego asks for, one
|
||||
<code>VARIABLE=value</code> per line (see <a href="https://go-acme.github.io/lego/dns/">the list of providers</a>), for example
|
||||
<code>AWS_ACCESS_KEY_ID=…</code> and <code>AWS_SECRET_ACCESS_KEY=…</code> for route53.</p>
|
||||
<form method="post" action="/dnsproviders">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
||||
<input type="hidden" name="providers" value="{{len .Rows}}">
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tr><th>Name</th><th>Provider</th><th>Login</th><th>Server</th><th>Password or API token</th><th>Certificate variables</th><th>Remove</th></tr>
|
||||
{{range $i, $r := .Rows}}
|
||||
<tr>
|
||||
<td>{{if $r.New}}<input name="prov.{{$i}}.name" size="14" spellcheck="false" placeholder="cloudflare"><input type="hidden" name="prov.{{$i}}.new" value="1">
|
||||
{{else}}{{$r.Name}}<input type="hidden" name="prov.{{$i}}.name" value="{{$r.Name}}">{{end}}</td>
|
||||
<td><select name="prov.{{$i}}.type">{{range $r.Options}}<option value="{{.Name}}" {{if .Selected}}selected{{end}}>{{.Label}}</option>{{end}}</select></td>
|
||||
<td><input name="prov.{{$i}}.username" value="{{$r.Username}}" size="12" spellcheck="false" autocomplete="off"></td>
|
||||
<td><input name="prov.{{$i}}.server" value="{{$r.Server}}" size="18" spellcheck="false" placeholder="only dyndns2"></td>
|
||||
<td><input type="password" name="prov.{{$i}}.secret" size="24" autocomplete="new-password" placeholder="{{if $r.SecretSet}}stored; leave empty to keep{{else if $r.New}}required{{else}}missing{{end}}"></td>
|
||||
<td><textarea name="prov.{{$i}}.env" rows="2" cols="26" spellcheck="false" autocomplete="off" placeholder="{{if $r.EnvSet}}stored; leave empty to keep{{else}}VARIABLE=value, one per line{{end}}"></textarea></td>
|
||||
<td>{{if not $r.New}}<input type="checkbox" name="prov.{{$i}}.remove">{{end}}</td>
|
||||
</tr>
|
||||
{{range $f := list "name" "type" "username" "server" "secret" "env"}}{{with index $e (printf "prov[%d].%s" $i $f)}}<tr><td colspan="7" class="field-error">{{$f}}: {{.}}</td></tr>{{end}}{{end}}
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
<p class="hint">Login is <code>token</code> for a Cloudflare API token (permission Zone → DNS → Edit). Fill in the last row to add an account.</p>
|
||||
<button>Save</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -13,14 +13,22 @@
|
||||
{{if .LoggedIn}}
|
||||
<nav>
|
||||
<a href="/">Overview</a>
|
||||
<a href="/network">Network</a>
|
||||
<a href="/dhcp">DHCP</a>
|
||||
<a href="/dns">DNS</a>
|
||||
<a href="/wireguard">WireGuard</a>
|
||||
<a href="/firewall">Firewall</a>
|
||||
<a href="/ddns">Dynamic DNS</a>
|
||||
<a href="/update">Update</a>
|
||||
<a href="/advanced">Advanced</a>
|
||||
<details name="menu"><summary>Network</summary><div>
|
||||
<a href="/network">Interfaces</a>
|
||||
<a href="/dhcp">DHCP</a>
|
||||
<a href="/dns">DNS</a>
|
||||
<a href="/wireguard">WireGuard</a>
|
||||
<a href="/firewall">Firewall</a>
|
||||
</div></details>
|
||||
<details name="menu"><summary>Services</summary><div>
|
||||
<a href="/ddns">Dynamic DNS</a>
|
||||
<a href="/dnsproviders">DNS providers</a>
|
||||
<a href="/certificate">Certificate</a>
|
||||
</div></details>
|
||||
<details name="menu"><summary>System</summary><div>
|
||||
<a href="/update">Update</a>
|
||||
<a href="/advanced">Advanced</a>
|
||||
</div></details>
|
||||
<form method="post" action="/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="link">Log out</button></form>
|
||||
</nav>
|
||||
{{end}}
|
||||
@@ -31,5 +39,6 @@
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
<script src="/static/menu.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user