This repository has been archived on 2026-10-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
karldns.de/karldns/web/templates_public.py
T

309 lines
11 KiB
Python
Raw Normal View History

2026-05-21 17:23:08 +02:00
from __future__ import annotations
from dataclasses import dataclass
2026-05-21 17:23:08 +02:00
from ..settings import DdnsSettings
from .components import _code_window, _example_update_ips, _message_band
from .html_utils import trusted
from .layout import PageOptions, _page, _top_nav
from .sections.public_home import _manual_updates_section, _public_home_hero, _setup_flow_section
2026-05-21 17:58:20 +02:00
from .templates_public_sections import _linux_installer_section
2026-05-21 17:23:08 +02:00
@dataclass(frozen=True, slots=True)
class _ApiDocsExamples:
create: str
register: str
update: str
status: str
rotate: str
schema: str
2026-05-21 17:23:08 +02:00
def _render_public_home(
settings: DdnsSettings,
message: str | None,
request_ip: str | None = None,
) -> str:
base_url = settings.public_base_url.rstrip("/")
hostname_suffix = settings.hostname_suffix or "your KarlDNS suffix"
demo_update_url = f"{base_url}/api/v1/router/update/<secret>?myip=<ipaddr>"
2026-05-21 17:23:08 +02:00
example_ipv4, _ = _example_update_ips(request_ip)
create_demo_command = f"curl -sS '{base_url}/api/v1/router/update/<secret>?myip={example_ipv4}'"
create_demo = f"{create_demo_command}\ngood {example_ipv4}"
manual_demo = "\n".join(
[
"# explicit IPv4",
f"curl -sS '{base_url}/api/v1/router/update/<secret>?myip={example_ipv4}'",
"# source IP fallback",
f"curl -sS '{base_url}/api/v1/router/update/<secret>'",
]
)
message_html = _message_band(message) if message else ""
description = (
"KarlDNS is a free self-hosted Dynamic DNS service for FRITZ!Box routers, "
"generated KarlDNS hostnames, Linux cron jobs, and home networks."
)
2026-05-21 17:23:08 +02:00
return _page(
"KarlDNS",
trusted(f"""
<main id="main">
{_top_nav()}
{_public_home_hero(suffix=hostname_suffix, demo_update_url=demo_update_url)}
2026-05-21 17:23:08 +02:00
{message_html}
{_setup_flow_section(create_demo=create_demo, create_demo_command=create_demo_command)}
{_manual_updates_section(manual_demo=manual_demo)}
2026-05-21 17:23:08 +02:00
{_linux_installer_section(base_url)}
</main>
"""),
PageOptions(
description=description,
preload_logo=True,
canonical_url=f"{base_url}/",
image_url=f"{base_url}/social-preview.png",
image_alt="KarlDNS landing page with private Dynamic DNS update URL",
structured_data={
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "KarlDNS",
"applicationCategory": "NetworkingApplication",
"operatingSystem": "Linux",
"url": f"{base_url}/",
"description": description,
"image": f"{base_url}/social-preview.png",
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "EUR"},
"featureList": [
"Dynamic DNS for FRITZ!Box routers",
"Accountless private update URLs",
"CNAME target for external DNS names",
"Linux cron updater",
],
},
),
2026-05-21 17:23:08 +02:00
)
2026-05-22 17:05:06 +02:00
2026-05-21 17:23:08 +02:00
def _render_privacy_page(settings: DdnsSettings) -> str:
base_url = settings.public_base_url.rstrip("/")
description = (
"Privacy and legal information for KarlDNS, including stored hostnames, update links, "
"IP history, and cleanup behavior."
)
2026-05-21 17:23:08 +02:00
return _page(
"Legal and Privacy",
trusted(f"""
<main id="main">
{_top_nav()}
<section class="hero-band compact">
<div class="container text-center d-grid justify-items-center gap-3">
<p class="eyebrow">Legal and privacy</p>
<h1>Privacy basics.</h1>
<p class="lead">KarlDNS stores only the data needed to keep your hostname updated.</p>
</div>
</section>
<section class="section">
<div class="container narrow d-grid gap-4">
<div>
<h2>Data processed</h2>
<p class="section-copy">
KarlDNS stores hostnames, protected dashboard tokens, private update links,
recent IP history, timestamps, and service events.
</p>
2026-05-21 17:23:08 +02:00
</div>
<div>
<h2>Secrets</h2>
<p class="section-copy">
Dashboard links, registration URLs, and update URLs are secrets.
Anyone with them may update or manage the hostname.
Dashboard lookup values are not stored as readable text.
</p>
2026-05-21 17:23:08 +02:00
</div>
<div>
<h2>Logs and retention</h2>
<p class="section-copy">
KarlDNS keeps recent update events for troubleshooting. Cleanup removes unused hostnames.
Server logs may also include IP addresses and request details.
</p>
2026-05-21 17:23:08 +02:00
</div>
<div>
<h2>Contact</h2>
<p class="section-copy">
For the public KarlDNS instance, contact Karl through
<a href="https://karl.fail" rel="noopener">karl.fail</a> or
<a href="https://karlcom.de" rel="noopener">karlcom.de</a>.
</p>
2026-05-21 17:23:08 +02:00
</div>
</div>
</section>
</main>
"""),
PageOptions(
description=description,
canonical_url=f"{base_url}/privacy",
image_url=f"{base_url}/social-preview.png",
image_alt="KarlDNS legal and privacy information",
structured_data={
"@context": "https://schema.org",
"@type": "WebPage",
"name": "KarlDNS Legal and Privacy",
"url": f"{base_url}/privacy",
"description": description,
"isPartOf": {
"@type": "WebSite",
"name": "KarlDNS",
"url": f"{base_url}/",
},
2026-05-21 17:23:08 +02:00
},
),
2026-05-21 17:23:08 +02:00
)
def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None) -> str:
base_url = settings.public_base_url.rstrip("/")
example_ipv4, _ = _example_update_ips(request_ip)
examples = _api_docs_examples(base_url, example_ipv4)
description = (
"Small KarlDNS API guide for creating, activating, updating, reading, and deleting "
"generated Dynamic DNS hostnames."
2026-05-22 17:05:06 +02:00
)
2026-05-21 17:23:08 +02:00
return _page(
"KarlDNS API",
trusted(f"""
<main id="main">
{_top_nav()}
{_api_docs_hero()}
{_api_docs_content(examples)}
2026-05-21 17:23:08 +02:00
</main>
"""),
PageOptions(
description=description,
canonical_url=f"{base_url}/docs",
image_url=f"{base_url}/social-preview.png",
image_alt="KarlDNS API documentation",
),
2026-05-21 17:23:08 +02:00
)
def _api_docs_examples(base_url: str, example_ipv4: str) -> _ApiDocsExamples:
return _ApiDocsExamples(
create=f"curl -sS -X POST {base_url}/api/v1/hostnames",
register=f"curl -sS -X POST '{base_url}/api/v1/router/register/<karl_dashboard_secret>?myip={example_ipv4}'",
update=f"curl -sS '{base_url}/api/v1/router/update/<karl_update_secret>?myip={example_ipv4}'",
status=f"curl -sS '{base_url}/api/v1/hostnames/<karl_dashboard_secret>?history_limit=10'",
rotate=f"curl -sS -X POST {base_url}/api/v1/hostnames/<karl_dashboard_secret>/rotate-update-url",
schema=f"curl -sS {base_url}/openapi.json",
)
def _api_docs_hero() -> str:
return """
<section class="hero-band compact">
<div class="container text-center d-grid justify-items-center gap-3">
<p class="eyebrow">API</p>
<h1>Small API surface.</h1>
<p class="lead">Create a hostname, register it, update it, and read its status.</p>
</div>
</section>
"""
def _api_docs_content(examples: _ApiDocsExamples) -> str:
return f"""
<section class="section section-compact">
<div class="container narrow d-grid gap-4">
{_api_endpoints_overview()}
{_api_example_block("Create", examples.create, "create command")}
{_api_example_block("Register", examples.register, "register command")}
{_api_example_block("Update", examples.update, "update command")}
{_api_status_block(examples.status)}
{_api_rotate_block(examples.rotate)}
{_api_schema_block(examples.schema)}
</div>
</section>
"""
def _api_endpoints_overview() -> str:
return """
<div class="api-endpoints">
<h2>Endpoints</h2>
<ol class="simple-steps text-start">
<li>
<strong>POST /api/v1/hostnames.</strong>
Create a pending hostname and private setup values. The response includes
<code>registration_url</code>.
</li>
<li>
<strong>GET or POST /api/v1/router/register/&lt;karl_secret&gt;.</strong>
Register the hostname after secrets are saved. Publishes an initial DNS record
from a public request IP or explicit <code>myip</code>/<code>myipv6</code> value.
If no public IP is visible, publishes a temporary <code>192.0.2.1</code> A record
until the first router update.
</li>
<li>
<strong>GET or POST /api/v1/router/update/&lt;karl_secret&gt;.</strong>
Router-friendly update endpoint. Supports <code>myip</code>, <code>ipaddr</code>,
<code>myipv6</code>, and <code>ip6addr</code>.
</li>
<li>
<strong>GET /api/v1/hostnames/&lt;karl_secret&gt;.</strong>
Read hostname status and recent history.
</li>
<li>
<strong>POST /api/v1/hostnames/&lt;karl_secret&gt;/rotate-update-url.</strong>
Replace the private router update URL.
</li>
<li>
<strong>DELETE /api/v1/hostnames/&lt;karl_secret&gt;.</strong>
Delete hostname and DNS records.
</li>
</ol>
</div>
"""
def _api_example_block(title: str, command: str, copy_label: str) -> str:
return f"""
<div>
<h2>{title}</h2>
{_code_window(command, copy_label=copy_label, class_name="mt-3")}
</div>
"""
def _api_status_block(command: str) -> str:
return f"""
<div>
<h2>Status</h2>
<p class="section-copy">
Use the private dashboard secret to read current DNS state and recent updates
for only that hostname.
</p>
{_code_window(command, copy_label="status command", class_name="mt-3")}
</div>
"""
def _api_rotate_block(command: str) -> str:
return f"""
<div>
<h2>Rotate update URL</h2>
<p class="section-copy">
Use the private dashboard secret to replace the router update URL.
The old update URL stops working immediately.
</p>
{_code_window(command, copy_label="rotate command", class_name="mt-3")}
</div>
"""
def _api_schema_block(command: str) -> str:
return f"""
<div>
<h2>Schema</h2>
<p class="section-copy">
Machine-readable OpenAPI is available for the public hostname and router endpoints.
</p>
{_code_window(command, copy_label="schema command", class_name="mt-3")}
</div>
"""