Clarify optional router credentials

This commit is contained in:
StasonJatham committed 2026-05-20 19:32:11 +02:00
1 parent 67477ab40f
commit f865bc28c7
3 files changed
+29 -21

No files matched your search

+6 -4
View File
@@ -144,13 +144,15 @@ export DDNS_TTL=60
## FRITZ!Box DynDNS Setup
Create a hostname in the web UI. RouterPulse shows the exact values for:
Create a hostname in the web UI. For the normal accountless flow, the generated `Update-URL` already contains the secret update slug. Put that URL into the router and use the generated hostname as `Domainnamen`.
RouterPulse also shows optional compatibility fields for routers that insist on separate credentials:
- `Update-URL`
- `Domainnamen`
- `CNAME target`
- `Benutzername`
- `Kennwort`
- `Benutzername` (optional)
- `Kennwort` (optional)
The generated FRITZ!Box URL uses the native placeholders:
@@ -158,7 +160,7 @@ The generated FRITZ!Box URL uses the native placeholders:
https://ddns.example.net/u/<random-update-slug>?myip=<ipaddr>&myipv6=<ip6addr>
```
This URL is enough for routers that only expose a single update URL field. RouterPulse also shows a `One-box update URL` that embeds `hostname`, `username`, and `password` as query parameters for clients that require credential-style DynDNS URLs but do not expose separate credential fields. Treat both URLs as secrets.
This URL is enough for routers that only expose a single update URL field and do not send separate credentials. RouterPulse also shows a `One-box update URL` that embeds `hostname`, `username`, and `password` as query parameters for clients that require credential-style DynDNS URLs but do not expose separate credential fields. Treat both URLs as secrets.
The compatibility endpoint is also available:
+13 -13
View File
@@ -463,8 +463,8 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
[
_copy_row("Domainnamen:", str(rotated["hostname"]), "Public"),
_copy_row("One-box update URL:", service.credentialed_update_url(rotated), "Secret"),
_copy_row("Benutzername:", str(rotated["username"]), "Public"),
_copy_row("Kennwort:", str(rotated["password"]), "Secret"),
_copy_row("Benutzername (optional):", str(rotated["username"]), "Public"),
_copy_row("Kennwort (optional):", str(rotated["password"]), "Secret"),
],
)
else:
@@ -820,12 +820,12 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
return f"""
<section class="section success-section">
<div class="container">
<p class="eyebrow">Credentials generated</p>
<p class="eyebrow">Hostname generated</p>
<h2>{html.escape(hostname)}</h2>
<p class="section-copy">Save this now. The password, private status page, and one-box update URL are only shown on this screen. To use your own DNS name, create a CNAME from that name to this hostname.</p>
<p class="section-copy">Use the Update-URL directly. The separate Benutzername and Kennwort values are optional compatibility fields for routers that require them. Save this now: the password, private status page, and one-box update URL are only shown on this screen.</p>
{_secret_notice()}
<div class="d-flex gap-2 flex-wrap my-3">
{_copy_button("Copy FRITZ!Box fields", fritzbox_fields)}
{_copy_button("Copy complete FRITZ!Box fields", fritzbox_fields)}
{_copy_button("Copy one-box update URL", credentialed_update_url)}
{_copy_button("Copy CNAME record", cname_record)}
</div>
@@ -834,8 +834,8 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
{_copy_row("One-box update URL:", credentialed_update_url, "Secret")}
{_copy_row("Domainnamen:", hostname, "Public")}
{_copy_row("CNAME target:", hostname, "Public")}
{_copy_row("Benutzername:", username, "Public")}
{_copy_row("Kennwort:", password, "Secret")}
{_copy_row("Benutzername (optional):", username, "Public")}
{_copy_row("Kennwort (optional):", password, "Secret")}
{_copy_row("Private status page:", management_url, "Secret")}
</div>
</div>
@@ -904,7 +904,7 @@ def _secret_notice() -> str:
return """
<div class="alert alert-info my-3 py-2 small" role="note">
<strong>Public:</strong> Domainnamen and CNAME target can be shared in DNS.
<strong>Secret:</strong> Update-URL, one-box update URL, Kennwort, private status page, and domain claim links control the hostname.
<strong>Secret:</strong> Update-URL, one-box update URL, optional Kennwort, private status page, and domain claim links control the hostname.
</div>
"""
@@ -956,11 +956,11 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage
<div class="row g-4 align-items-start">
<div class="col-lg-6">
<p class="eyebrow">Router settings</p>
<h2>FRITZ!Box fields</h2>
<p class="section-copy">Use the Update-URL directly if your router only has one URL field. The username and password are shown only when generated or rotated. If you use your own DNS name, point it at this hostname with a CNAME.</p>
<h2>Router update settings</h2>
<p class="section-copy">Use the Update-URL directly if your router only has one URL field. Separate Benutzername and Kennwort values are optional compatibility fields and are only shown when generated or rotated. If you use your own DNS name, point it at this hostname with a CNAME.</p>
{_secret_notice()}
<div class="d-flex gap-2 flex-wrap mt-3">
{_copy_button("Copy FRITZ!Box fields", fritzbox_fields)}
{_copy_button("Copy complete FRITZ!Box fields", fritzbox_fields)}
{_copy_button("Copy CNAME record", cname_record)}
</div>
</div>
@@ -968,8 +968,8 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage
{_copy_row("Update-URL:", update_url, "Secret")}
{_copy_row("Domainnamen:", hostname, "Public")}
{_copy_row("CNAME target:", hostname, "Public")}
{_copy_row("Benutzername:", username, "Public")}
{_copy_row("Kennwort:", "unchanged; only shown when generated", "Secret")}
{_copy_row("Benutzername (optional):", username, "Public")}
{_copy_row("Kennwort (optional):", "unchanged; only shown when generated", "Secret")}
</div>
</div>
</div>
+10 -4
View File
@@ -344,7 +344,7 @@ def test_admin_can_rotate_links_and_password(tmp_path: Path) -> None:
assert "Router password rotated" in password.text
assert "One-box update URL:" in password.text
assert "/nic/update?" in password.text
assert "Kennwort:" in password.text
assert "Kennwort (optional):" in password.text
def test_self_service_managed_hostname_generates_random_account(tmp_path: Path) -> None:
@@ -364,7 +364,9 @@ def test_self_service_managed_hostname_generates_random_account(tmp_path: Path)
assert "Update-URL:" in response.text
assert "One-box update URL:" in response.text
assert "CNAME target:" in response.text
assert "Copy FRITZ!Box fields" in response.text
assert "Benutzername (optional):" in response.text
assert "Kennwort (optional):" in response.text
assert "Copy complete FRITZ!Box fields" in response.text
assert "Copy CNAME record" in response.text
assert "Public:" in response.text
assert "Secret:" in response.text
@@ -391,8 +393,10 @@ def test_magic_hostname_without_login_generates_update_and_management_links(tmp_
assert "Update-URL:" in response.text
assert "One-box update URL:" in response.text
assert "CNAME target:" in response.text
assert "Benutzername (optional):" in response.text
assert "Kennwort (optional):" in response.text
assert "Private status page:" in response.text
assert "Copy FRITZ!Box fields" in response.text
assert "Copy complete FRITZ!Box fields" in response.text
assert "Copy CNAME record" in response.text
assert response.text.count("Secret") >= 3
assert response.text.count("Public") >= 3
@@ -420,7 +424,9 @@ def test_magic_management_link_can_delete_hostname(tmp_path: Path) -> None:
assert management.status_code == 200
assert "home.ddns.example.net" in management.text
assert "CNAME target:" in management.text
assert "Copy FRITZ!Box fields" in management.text
assert "Benutzername (optional):" in management.text
assert "Kennwort (optional):" in management.text
assert "Copy complete FRITZ!Box fields" in management.text
assert "Copy CNAME record" in management.text
assert "Public:" in management.text
assert "Secret:" in management.text