Archived
Add footer and privacy page
This commit is contained in:
1 parent
1759415f78
commit
49b9e3a7ca
2 files changed
+84
-7
No files matched your search
+63
-7
@@ -515,6 +515,10 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
|
|||||||
def healthz() -> str:
|
def healthz() -> str:
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
|
@app.get("/privacy", response_class=HTMLResponse, include_in_schema=False)
|
||||||
|
def privacy() -> str:
|
||||||
|
return _render_privacy_page()
|
||||||
|
|
||||||
@app.get("/favicon.ico", include_in_schema=False)
|
@app.get("/favicon.ico", include_in_schema=False)
|
||||||
def favicon() -> FileResponse:
|
def favicon() -> FileResponse:
|
||||||
return FileResponse(ASSET_DIR / "favicon.ico", media_type="image/x-icon")
|
return FileResponse(ASSET_DIR / "favicon.ico", media_type="image/x-icon")
|
||||||
@@ -757,12 +761,6 @@ def _render_public_home(
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{challenge_html}
|
{challenge_html}
|
||||||
<footer class="py-4 bg-body border-top">
|
|
||||||
<div class="container nav justify-content-center gap-3">
|
|
||||||
<a class="nav-link p-0 small text-secondary" href="/docs">API docs</a>
|
|
||||||
<a class="nav-link p-0 small text-secondary" href="/redoc">ReDoc</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</main>
|
</main>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
@@ -1353,6 +1351,62 @@ def _user_event_row(event: dict[str, str | None]) -> str:
|
|||||||
return f"<tr><td>{created}</td><td>{status}</td><td>{ipv4}</td><td>{ipv6}</td><td>{detail}</td></tr>"
|
return f"<tr><td>{created}</td><td>{status}</td><td>{ipv4}</td><td>{ipv6}</td><td>{detail}</td></tr>"
|
||||||
|
|
||||||
|
|
||||||
|
def _render_privacy_page() -> str:
|
||||||
|
return _page(
|
||||||
|
"Legal and Privacy",
|
||||||
|
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>KarlDNS keeps accountless DynDNS simple.</h1>
|
||||||
|
<p class="lead">This page explains the basic data handled by this self-hosted service. Configure your own imprint, controller details, and jurisdiction-specific terms before offering it publicly.</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 generated hostnames, public DNS target names, update slugs, hashed management tokens, hashed router passwords, DNS TXT verification challenges, IP update history, timestamps, and operational events needed to run Dynamic DNS.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Secrets</h2>
|
||||||
|
<p class="section-copy">Private management links, update URLs, router passwords, and one-box update URLs are bearer secrets. Anyone with them can update or manage the related hostname. Passwords and management lookup values are not stored in recoverable plaintext.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Logs and retention</h2>
|
||||||
|
<p class="section-copy">The service keeps recent update events for troubleshooting and runs cleanup jobs for abandoned setup flows and unused hostnames. Server and reverse-proxy logs may also contain IP addresses and request metadata depending on your deployment.</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _site_footer() -> str:
|
||||||
|
return """
|
||||||
|
<footer class="site-footer border-top">
|
||||||
|
<div class="container d-flex align-items-center justify-content-between gap-3 flex-column flex-md-row">
|
||||||
|
<div class="small text-secondary">Made with ❤️ by Karl</div>
|
||||||
|
<nav class="nav justify-content-center gap-3" aria-label="Footer links">
|
||||||
|
<a class="nav-link p-0 small text-secondary" href="https://github.com/StasonJatham/router-dyndns" rel="noopener">GitHub</a>
|
||||||
|
<a class="nav-link p-0 small text-secondary" href="https://karl.fail" rel="noopener">karl.fail</a>
|
||||||
|
<a class="nav-link p-0 small text-secondary" href="https://karlcom.de" rel="noopener">karlcom.de</a>
|
||||||
|
<a class="nav-link p-0 small text-secondary" href="/docs">API docs</a>
|
||||||
|
<a class="nav-link p-0 small text-secondary" href="/redoc">ReDoc</a>
|
||||||
|
<a class="nav-link p-0 small text-secondary" href="/privacy">Legal & privacy</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _page(title: str, body: str) -> str:
|
def _page(title: str, body: str) -> str:
|
||||||
return f"""
|
return f"""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -1480,6 +1534,8 @@ def _page(title: str, body: str) -> str:
|
|||||||
.admin-table-scroll {{ max-height: 560px; overflow: auto; }}
|
.admin-table-scroll {{ max-height: 560px; overflow: auto; }}
|
||||||
.admin-table-scroll thead th {{ position: sticky; top: 0; z-index: 1; background: var(--rp-surface); }}
|
.admin-table-scroll thead th {{ position: sticky; top: 0; z-index: 1; background: var(--rp-surface); }}
|
||||||
.table-page-size {{ width: auto; min-width: 76px; }}
|
.table-page-size {{ width: auto; min-width: 76px; }}
|
||||||
|
.site-footer {{ padding: 24px 0; background: var(--rp-bg); }}
|
||||||
|
.site-footer .nav-link:hover {{ color: var(--rp-ink) !important; }}
|
||||||
table {{ width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: tabular-nums; color: var(--rp-ink); }}
|
table {{ width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: tabular-nums; color: var(--rp-ink); }}
|
||||||
th, td {{ border-bottom: 1px solid var(--rp-soft-line); padding: 14px 10px; text-align: left; white-space: nowrap; }}
|
th, td {{ border-bottom: 1px solid var(--rp-soft-line); padding: 14px 10px; text-align: left; white-space: nowrap; }}
|
||||||
tr:last-child th, tr:last-child td {{ border-bottom: 0; }}
|
tr:last-child th, tr:last-child td {{ border-bottom: 0; }}
|
||||||
@@ -1507,6 +1563,6 @@ def _page(title: str, body: str) -> str:
|
|||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>{body}</body>
|
<body>{body}{_site_footer()}</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -609,6 +609,27 @@ def test_application_pages_use_stricter_script_csp(tmp_path: Path) -> None:
|
|||||||
assert client.get("/app.js").status_code == 200
|
assert client.get("/app.js").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_footer_links_and_privacy_page_are_available(tmp_path: Path) -> None:
|
||||||
|
app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin"))
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
response = client.get("/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Made with ❤️ by Karl" in response.text
|
||||||
|
assert 'href="https://github.com/StasonJatham/router-dyndns"' in response.text
|
||||||
|
assert 'href="https://karl.fail"' in response.text
|
||||||
|
assert 'href="https://karlcom.de"' in response.text
|
||||||
|
assert 'href="/privacy"' in response.text
|
||||||
|
|
||||||
|
privacy = client.get("/privacy")
|
||||||
|
assert privacy.status_code == 200
|
||||||
|
assert "Legal and privacy" in privacy.text
|
||||||
|
assert "Data processed" in privacy.text
|
||||||
|
assert "hashed router passwords" in privacy.text
|
||||||
|
assert "Made with ❤️ by Karl" in privacy.text
|
||||||
|
|
||||||
|
|
||||||
def test_web_icon_assets_and_manifest_are_exposed(tmp_path: Path) -> None:
|
def test_web_icon_assets_and_manifest_are_exposed(tmp_path: Path) -> None:
|
||||||
app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin"))
|
app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin"))
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|||||||
Reference in new issue
Block a user