+
+
+
FRITZ!Box compatible
-
Free DynDNS for {suffix}
-
Generate cryptographically random URLs, paste the update URL into your router, and keep the management link somewhere safe.
+
DynDNS without the account ceremony.
+
Create a secure update endpoint under {suffix}. Provider hostnames are anonymous; custom domains require DNS proof.
+
- Admin
{message_html}
{created_html}
- {managed_html}
- {challenge_html}
+
+
+
+
+
Custom domains
+
Use your own domain after TXT verification.
+
Sign in to prove ownership and create hostnames below verified DNS zones.
+
+
Sign in or register
+
+
+
""",
)
-def _render_management_page(service: DdnsService, settings: DdnsSettings, management_slug: str, account: dict[str, str | int | None]) -> str:
- account_for_url = {
- "hostname": str(account["hostname"]),
- "username": str(account["username"]),
- "password": "",
- "update_slug": str(account["update_slug"]),
- "management_slug": management_slug,
- }
- update_url = html.escape(service.fritz_update_url(account_for_url))
- hostname = html.escape(str(account["hostname"]))
- username = html.escape(str(account["username"]))
- ipv4 = html.escape(str(account.get("ipv4") or "-"))
- ipv6 = html.escape(str(account.get("ipv6") or "-"))
- updated = html.escape(str(account.get("updated_at") or "Never"))
+def _render_user_home(
+ service: DdnsService,
+ settings: DdnsSettings,
+ user: dict[str, str | int],
+ accounts: list[dict[str, str | int | None]],
+ created_account: dict[str, str] | None,
+ message: str | None,
+ challenge: dict[str, str | None] | None = None,
+) -> str:
+ created_html = _credentials_panel(service, created_account)
+ message_html = _message_band(message) if message and not challenge else ""
+ account_rows = "\n".join(_account_row(account, show_owner=False) for account in accounts) or """
+
No hostnames yet.
+ """
return _page(
- "DynDNS management",
+ "router-dyndns dashboard",
f"""
-
-
-
-
Magic management
-
{hostname}
-
Anyone with this link can manage this hostname. Keep it private.
-
- Home
-
-
- FRITZ!Box fields
-
-
Update-URL:
-
Domainnamen:
-
Benutzername:
-
Kennwort:
+
+
+
+ {html.escape(str(user["email"]))}
+
-
- Current IP
-
-
- IPv4 {ipv4}
- IPv6 {ipv6}
- Updated {updated}
-
-
-
+
+
+
Dashboard
+
Your router hostnames
+
Create provider hostnames immediately or verify a custom domain with DNS before issuing credentials.
+
+
+ {message_html}
+ {created_html}
+
+ {_custom_domain_flow(service, challenge, message)}
+
+
+
+
+
Inventory
+
Hostnames
+
+
+
+
+ Domain User IPv4 IPv6 Updated
+ {account_rows}
+
+
+
+
+
+ """,
+ )
+
+
+def _render_auth_page(settings: DdnsSettings, message: str | None) -> str:
+ invite_field = """
+
Invite code
+
+
+ """ if settings.require_invite else ""
+ return _page(
+ "Sign in",
+ f"""
+
+
+
+
Custom domains
+
Sign in to verify domains.
+
Provider-owned hostnames are anonymous. Custom domains need an account so TXT claims and quotas stay scoped to you.
+
+
+ {_message_band(message) if message else ""}
+
+
+
+
Login
+
+
+
+
Register
+
+
+
""",
@@ -480,68 +595,27 @@ def _render_public_page(
message: str | None,
created_account: dict[str, str] | None = None,
) -> str:
- suffix = html.escape(settings.hostname_suffix or "your domain")
- message_html = f'
' if message else ""
- if not user:
- invite_field = """
-
Invite code
-
-
- """ if settings.require_invite else ""
- return _page(
- "DynDNS",
- f"""
-
-
-
-
FRITZ!Box compatible
-
Managed DynDNS for {suffix}
-
Create a secure update URL for your FRITZ!Box or router. Sign in to generate provider hostnames or verify your own domain with DNS.
-
- Admin
-
- {message_html}
-
-
-
Login
-
-
-
-
Register
-
-
-
-
- """,
- )
- return _render_magic_page(service, settings, created_account, message, challenge)
+ if user:
+ return _render_user_home(service, settings, user, accounts, created_account, message, challenge)
+ return _render_auth_page(settings, message)
-def _challenge_panel(service: DdnsService, challenge: dict[str, str | None] | None, message: str | None) -> str:
+def _custom_domain_flow(service: DdnsService, challenge: dict[str, str | None] | None, message: str | None) -> str:
challenge_html = ""
- claim_secret = ""
+ create_form = ""
if challenge:
domain = html.escape(str(challenge["domain"]))
- token = html.escape(str(challenge["token"]))
+ token = str(challenge["token"])
claim_secret = html.escape(str(challenge.get("claim_secret") or ""))
- verification_name = html.escape(service.verification_name(str(challenge["domain"])))
+ verification_name = service.verification_name(str(challenge["domain"]))
status = html.escape(message or "Add this TXT record, then verify.")
challenge_html = f"""
-
+
+
2
+
Add TXT record
{status}
-
- TXT name
- TXT value
-
+ {_copy_row("TXT name", verification_name)}
+ {_copy_row("TXT value", token)}
"""
+ if challenge.get("verified_at"):
+ create_form = f"""
+
+ 3
+
Create hostname
+
+
+ """
+ else:
+ create_form = """
+
+
3
+
Create hostname
+
Credentials unlock after the TXT record verifies.
+
+ """
return f"""
-
- Custom domain
-
- {challenge_html}
-
- The hostname must be inside a domain that has already passed TXT verification.
+
+
+
+
+
Custom domain
+
Verify ownership before issuing credentials.
+
The hostname must be inside a DNS zone this service can publish.
+
+
+
+
+ 1
+
Add domain
+
+
+ {challenge_html}
+ {create_form}
+
+
+
+ """
+
+
+def _challenge_panel(service: DdnsService, challenge: dict[str, str | None] | None, message: str | None) -> str:
+ return _custom_domain_flow(service, challenge, message)
+
+
+def _credentials_panel(service: DdnsService, created_account: dict[str, str] | None) -> str:
+ if not created_account:
+ return ""
+ hostname = created_account["hostname"]
+ username = created_account["username"]
+ password = created_account["password"]
+ update_url = service.fritz_update_url(created_account)
+ management_url = service.magic_management_url(created_account)
+ return f"""
+
+
+
Account generated
+
{html.escape(hostname)}
+
Save this now. The password and management link are only shown on this screen.
+
+ {_copy_row("Update-URL:", update_url)}
+ {_copy_row("Domainnamen:", hostname)}
+ {_copy_row("Benutzername:", username)}
+ {_copy_row("Kennwort:", password)}
+ {_copy_row("Magic management link:", management_url)}
+
+
"""
def _created_account_panel(service: DdnsService, created_account: dict[str, str] | None) -> str:
- if not created_account:
- return ""
- hostname = html.escape(created_account["hostname"])
- username = html.escape(created_account["username"])
- password = html.escape(created_account["password"])
- update_url = html.escape(service.fritz_update_url(created_account))
- management_url = html.escape(service.magic_management_url(created_account))
+ return _credentials_panel(service, created_account)
+
+
+def _copy_row(label: str, value: str) -> str:
+ safe_label = html.escape(label)
+ safe_value = html.escape(value)
return f"""
-
-
-
Account generated
-
{hostname}
-
-
- Update-URL:
- Domainnamen:
- Benutzername:
- Kennwort:
- Magic management link:
-
- Save this now. The password and management link are only shown on this screen. The Update-URL and management link are separate cryptographically random bearer URLs.
+
+ {safe_label}
+ {safe_value}
+ Copy
+
+ """
+
+
+def _message_band(message: str | None) -> str:
+ if not message:
+ return ""
+ return f"""
+
"""
+def _render_management_page(service: DdnsService, settings: DdnsSettings, management_slug: str, account: dict[str, str | int | None]) -> str:
+ account_for_url = {
+ "hostname": str(account["hostname"]),
+ "username": str(account["username"]),
+ "password": "",
+ "update_slug": str(account["update_slug"]),
+ "management_slug": management_slug,
+ }
+ update_url = service.fritz_update_url(account_for_url)
+ hostname = html.escape(str(account["hostname"]))
+ username = html.escape(str(account["username"]))
+ ipv4 = html.escape(str(account.get("ipv4") or "-"))
+ ipv6 = html.escape(str(account.get("ipv6") or "-"))
+ updated = html.escape(str(account.get("updated_at") or "Never"))
+ return _page(
+ "DynDNS management",
+ f"""
+
+
+
+
Magic management
+
{hostname}
+
Anyone with this link can manage or delete this hostname. Keep it private.
+
Home
+
+
+
+
+
+
+
Current address
+
Status
+
+
+
+ IPv4 {ipv4}
+ IPv6 {ipv6}
+ Updated {updated}
+
+
+
+
+
+
+
+
Danger zone
+
Delete hostname
+
This removes the account and DNS records. The router update URL will stop working.
+
+
+
+
+
+ """,
+ )
+
+
def _render_admin_page(
service: DdnsService,
settings: DdnsSettings,
@@ -624,59 +830,84 @@ def _render_admin_page(
return _page(
"DynDNS Admin",
f"""
-
-
-
+
+
+
+
DynDNS aktiv
FRITZ!Box accounts
-
-
{_created_account_panel(service, created_account)}
-
- Invites
-
-
-
- Code Created Used
- {invite_rows}
-
+
-
- Generate account
- Geben Sie die Anmeldedaten für Ihren DynDNS-Anbieter an. Generated accounts produce the FRITZ!Box fields: Update-URL, Domainnamen, Benutzername, Kennwort.
-
- {suffix_help}
+
+
+
+
+
Invites
+
Registration codes
+
+
+
+
+
+ Code Created Used
+ {invite_rows}
+
+
+
-
- Accounts
-
-
-
- Domain User Owner IPv4 IPv6 Updated
-
- {rows}
-
+
+
+
+
+
Accounts
+
Active credentials
+
+
+
+
+
+ Domain User Owner IPv4 IPv6 Updated
+
+ {rows}
+
+
@@ -723,61 +954,112 @@ def _page(title: str, body: str) -> str:
+
{body}