diff --git a/README.md b/README.md index d6e0f7a..1a494e2 100644 --- a/README.md +++ b/README.md @@ -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/?myip=&myipv6= ``` -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: diff --git a/router_dyndns/ddns.py b/router_dyndns/ddns.py index 914b042..c251a78 100644 --- a/router_dyndns/ddns.py +++ b/router_dyndns/ddns.py @@ -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"""
-

Credentials generated

+

Hostname generated

{html.escape(hostname)}

-

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.

+

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.

{_secret_notice()}
- {_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)}
@@ -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")}
@@ -904,7 +904,7 @@ def _secret_notice() -> str: return """
Public: Domainnamen and CNAME target can be shared in DNS. - Secret: Update-URL, one-box update URL, Kennwort, private status page, and domain claim links control the hostname. + Secret: Update-URL, one-box update URL, optional Kennwort, private status page, and domain claim links control the hostname.
""" @@ -956,11 +956,11 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage

Router settings

-

FRITZ!Box fields

-

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.

+

Router update settings

+

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.

{_secret_notice()}
- {_copy_button("Copy FRITZ!Box fields", fritzbox_fields)} + {_copy_button("Copy complete FRITZ!Box fields", fritzbox_fields)} {_copy_button("Copy CNAME record", cname_record)}
@@ -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")}
diff --git a/tests/test_ddns.py b/tests/test_ddns.py index d944b7c..d09ad56 100644 --- a/tests/test_ddns.py +++ b/tests/test_ddns.py @@ -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