2026-05-20 23:55:27 +02:00
from __future__ import annotations
2026-05-21 17:23:08 +02:00
from ..services.ddns import DdnsService
from ..settings import DdnsSettings
from .components import (
2026-05-21 17:58:20 +02:00
_code_window ,
2026-05-21 14:18:39 +02:00
_copy_menu ,
2026-05-20 23:55:27 +02:00
_copy_row ,
2026-05-21 17:23:08 +02:00
_event_label ,
2026-05-20 23:55:27 +02:00
_fritzbox_fields ,
_manual_update_commands ,
_manual_update_panel ,
_secret_notice ,
)
2026-05-21 17:23:08 +02:00
from .html_utils import SafeHtml , attr , escape , trusted
from .layout import _page , _top_nav
2026-05-20 23:55:27 +02:00
def _render_management_page (
service : DdnsService ,
settings : DdnsSettings ,
management_slug : str ,
account : dict [ str , str | int | None ],
csrf_token : str ,
message : str | None = None ,
2026-05-21 17:23:08 +02:00
request_ip : str | None = None ,
2026-05-20 23:55:27 +02:00
) -> str :
account_for_url = {
"hostname" : str ( account [ "hostname" ]),
"update_slug" : str ( account [ "update_slug" ]),
"management_slug" : management_slug ,
}
update_url = service . fritz_update_url ( account_for_url )
hostname = escape ( account [ "hostname" ])
ipv4 = escape ( account . get ( "ipv4" ) or "-" )
ipv6 = escape ( account . get ( "ipv6" ) or "-" )
updated = escape ( account . get ( "updated_at" ) or "Never" )
2026-05-21 17:23:08 +02:00
fritzbox_fields = _fritzbox_fields ( update_url , hostname )
2026-05-20 23:55:27 +02:00
cname_record = f "your-subdomain.example.com. CNAME { hostname } ."
2026-05-21 17:23:08 +02:00
manual_commands = _manual_update_commands ( update_url , request_ip )
2026-05-20 23:55:27 +02:00
history_rows = " \n " . join ( _user_event_row ( event ) for event in service . store . list_update_events_for_hostname ( str ( account [ "hostname" ]), 10 )) or """
<tr><td colspan="6" class="empty">No router updates yet.</td></tr>
"""
message_html = _management_message ( message )
dashboard_password_panel = _dashboard_password_panel ( management_slug , csrf_token , bool ( account . get ( "dashboard_password_hash" )))
return _page (
"KarlDNS management" ,
trusted ( f """
<main id="main">
{ _top_nav () }
<section class="hero-band compact">
<div class="container text-center d-grid justify-items-center gap-3">
2026-05-21 10:05:40 +02:00
<p class="eyebrow">Dashboard</p>
2026-05-20 23:55:27 +02:00
<h1> { hostname } </h1>
2026-05-21 10:05:40 +02:00
<p class="lead">This private link can manage this hostname. Keep it safe.</p>
<a class="btn btn-outline-primary rounded-pill" href="/">Home</a>
2026-05-20 23:55:27 +02:00
</div>
</section>
{ message_html }
<section class="section">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-6">
<p class="eyebrow">Router settings</p>
<h2>Router update settings</h2>
2026-05-21 17:23:08 +02:00
<p class="section-copy">Use this Update-URL in your router or script. Optional: point another DNS name at this hostname with a CNAME.</p>
2026-05-20 23:55:27 +02:00
{ _secret_notice () }
2026-05-21 14:18:39 +02:00
<div class="copy-actions mt-3">
{ _copy_menu ( "Copy setup values" , [( "Router fields" , fritzbox_fields ), ( "CNAME" , cname_record )]) }
2026-05-20 23:55:27 +02:00
</div>
</div>
<div class="col-lg-6 d-grid gap-2">
{ _copy_row ( "Update-URL:" , update_url , "Secret" ) }
{ _copy_row ( "Domainnamen:" , hostname , "Public" ) }
{ _copy_row ( "CNAME target:" , hostname , "Public" ) }
</div>
</div>
{ _manual_update_panel ( manual_commands ) }
<div class="card card-body gap-3 mt-4">
<div>
<p class="eyebrow">Actions</p>
2026-05-21 17:23:08 +02:00
<h3>Replace router update URL</h3>
<p class="section-copy">Create a new update URL. Update your router after changing it.</p>
2026-05-20 23:55:27 +02:00
</div>
<div class="d-flex gap-2 flex-wrap">
<form method="post" action="/m/ { attr ( management_slug ) } /rotate">
<input type="hidden" name="csrf" value=" { attr ( csrf_token ) } ">
<input type="hidden" name="action" value="update">
2026-05-21 10:05:40 +02:00
<button class="btn btn-outline-primary rounded-pill" type="submit">New update URL</button>
2026-05-20 23:55:27 +02:00
</form>
</div>
</div>
</div>
</section>
{ dashboard_password_panel }
<section class="section">
<div class="container d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
<div>
<p class="eyebrow">Current address</p>
<h2>Status</h2>
</div>
<div class="table-responsive status-table-shell">
<table class="table status-table mb-0">
<tbody>
<tr><th>IPv4</th><td> { ipv4 } </td></tr>
<tr><th>IPv6</th><td> { ipv6 } </td></tr>
<tr><th>Updated</th><td> { updated } </td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
<div>
<p class="eyebrow">History</p>
<h2>Router updates</h2>
2026-05-21 10:05:40 +02:00
<p class="section-copy">Recent IP changes and failed updates for this hostname.</p>
2026-05-20 23:55:27 +02:00
</div>
</div>
<div class="table-responsive border rounded admin-table-scroll">
<table class="table mb-0">
<thead><tr><th>Time</th><th>Status</th><th>IPv4</th><th>IPv6</th><th>Count</th><th>Detail</th></tr></thead>
<tbody> { history_rows } </tbody>
</table>
</div>
</div>
</section>
<section class="section danger-section">
<div class="container d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
<div>
2026-05-21 10:05:40 +02:00
<p class="eyebrow">Delete</p>
2026-05-20 23:55:27 +02:00
<h2>Delete hostname</h2>
<p class="section-copy">This removes the hostname and DNS records. The router update URL will stop working.</p>
</div>
<form method="post" action="/m/ { attr ( management_slug ) } /delete">
<input type="hidden" name="csrf" value=" { attr ( csrf_token ) } ">
<button class="btn btn-outline-danger rounded-pill" type="submit">Delete hostname</button>
</form>
</div>
</section>
</main>
""" ),
2026-05-21 18:25:32 +02:00
extra_styles = trusted ( '<link href="/admin.css?v=3" rel="stylesheet">' ),
2026-05-20 23:55:27 +02:00
)
2026-05-21 17:23:08 +02:00
def _render_pending_management_page (
2026-05-21 17:58:20 +02:00
service : DdnsService ,
2026-05-21 17:23:08 +02:00
settings : DdnsSettings ,
management_slug : str ,
account : dict [ str , str | int | None ],
csrf_token : str ,
message : str | None = None ,
2026-05-21 17:58:20 +02:00
request_ip : str | None = None ,
2026-05-21 17:23:08 +02:00
) -> str :
2026-05-21 17:58:20 +02:00
raw_hostname = str ( account [ "hostname" ])
account_for_url = {
"hostname" : raw_hostname ,
"update_slug" : str ( account [ "update_slug" ]),
"management_slug" : management_slug ,
}
update_url = service . fritz_update_url ( account_for_url )
dashboard_url = service . dashboard_url ({ "management_slug" : management_slug })
hostname = escape ( raw_hostname )
2026-05-21 17:23:08 +02:00
message_html = _management_message ( message )
cleanup_hours = settings . cleanup_pending_account_hours
cleanup_text = f " { cleanup_hours } hours" if cleanup_hours != 24 else "24 hours"
2026-05-21 17:58:20 +02:00
manual_commands = _manual_update_commands ( update_url , request_ip )
2026-05-21 17:23:08 +02:00
return _page (
2026-05-21 17:58:20 +02:00
"KarlDNS setup" ,
2026-05-21 17:23:08 +02:00
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">Pending setup</p>
<h1> { hostname } </h1>
2026-05-21 17:58:20 +02:00
<p class="lead">Save these values, then register the hostname. Pending setups are cleaned up after { escape ( cleanup_text ) } .</p>
2026-05-21 17:23:08 +02:00
</div>
</section>
{ message_html }
2026-05-21 17:58:20 +02:00
<section class="section">
<div class="container narrow d-grid gap-4">
<div>
<p class="eyebrow">Save</p>
<h2>Your values.</h2>
2026-05-21 18:30:15 +02:00
<p class="section-copy">Save the update URL and dashboard link before you register. After registration, the dashboard hides secrets by default.</p>
<p class="section-copy"><strong>Public:</strong> Domainnamen. <strong>Secret:</strong> Update-URL and private dashboard.</p>
2026-05-21 17:58:20 +02:00
</div>
<div class="d-grid gap-2">
{ _copy_row ( "Update-URL:" , update_url , "Secret" , mask_secret = False ) }
{ _copy_row ( "Domainnamen:" , raw_hostname , "Public" ) }
{ _copy_row ( "Private dashboard:" , dashboard_url , "Secret" , mask_secret = False ) }
</div>
<form method="post" action="/m/ { attr ( management_slug ) } /activate">
<input type="hidden" name="csrf" value=" { attr ( csrf_token ) } ">
<button class="btn btn-primary rounded-pill" type="submit">Register hostname</button>
</form>
</div>
</section>
2026-05-21 17:23:08 +02:00
<section class="section section-compact">
2026-05-21 17:58:20 +02:00
<div class="container narrow d-grid gap-4">
<div>
<p class="eyebrow">FRITZ!Box</p>
<h2>Router setup.</h2>
<p class="section-copy">Paste this exact URL into the FRITZ!Box <strong>Update-URL</strong> field. Use <strong> { hostname } </strong> as <strong>Domainnamen</strong>. No KarlDNS username or password is needed; if your router requires them, enter any placeholder.</p>
{ _code_window ( update_url , copy_label = "FRITZ!Box update URL" , class_name = "mt-3" ) }
</div>
<div>
<h2>Manual updates.</h2>
2026-05-21 18:57:45 +02:00
<p class="section-copy">Scripts can send the IP explicitly, or omit it and let KarlDNS use the request IP. Visible examples use your public IP when KarlDNS or your browser can detect it.</p>
2026-05-21 17:58:20 +02:00
{ _code_window ( manual_commands , copy_label = "manual update commands" , class_name = "mt-3" ) }
</div>
<div>
<h2>Use your own DNS name.</h2>
2026-05-21 18:30:15 +02:00
<p class="section-copy">Create a CNAME at your DNS provider that points to <strong> { hostname } </strong>. This is the same value as Domainnamen.</p>
2026-05-21 17:58:20 +02:00
</div>
2026-05-21 17:23:08 +02:00
</div>
</section>
</main>
""" ),
)
2026-05-20 23:55:27 +02:00
def _management_message ( message : str | None ) -> SafeHtml :
if not message :
return trusted ( "" )
return trusted (
f """
<div class="alert alert-info mb-0 rounded-0 border-start-0 border-end-0" role="status">
<div class="container"> { escape ( message ) } </div>
</div>
"""
)
def _dashboard_password_panel ( management_slug : str , csrf_token : str , password_enabled : bool ) -> SafeHtml :
password_status = "Enabled" if password_enabled else "Not enabled"
2026-05-21 10:05:40 +02:00
password_action = "Change password" if password_enabled else "Set password"
2026-05-20 23:55:27 +02:00
remove_form = ""
if password_enabled :
remove_form = f """
<form class="card card-body gap-3" method="post" action="/m/ { attr ( management_slug ) } /password/remove">
<input type="hidden" name="csrf" value=" { attr ( csrf_token ) } ">
2026-05-21 10:05:40 +02:00
<label class="form-label" for="dashboard-remove-current-password">Current password</label>
2026-05-20 23:55:27 +02:00
<input class="form-control" id="dashboard-remove-current-password" name="current_dashboard_password" type="password" autocomplete="current-password" required>
2026-05-21 10:05:40 +02:00
<button class="btn btn-outline-danger rounded-pill" type="submit">Remove password</button>
2026-05-20 23:55:27 +02:00
</form>
"""
current_password_field = ""
if password_enabled :
current_password_field = """
<div>
2026-05-21 10:05:40 +02:00
<label class="form-label" for="dashboard-current-password">Current password</label>
2026-05-20 23:55:27 +02:00
<input class="form-control" id="dashboard-current-password" name="current_dashboard_password" type="password" autocomplete="current-password" required>
</div>
"""
return trusted (
f """
<section class="section">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-5">
<p class="eyebrow">Dashboard password</p>
<h2>Password protection</h2>
2026-05-21 10:05:40 +02:00
<p class="section-copy">Status: { escape ( password_status ) } . This password protects only this dashboard. Router updates still work.</p>
2026-05-20 23:55:27 +02:00
</div>
<div class="col-lg-7 d-grid gap-3">
<form class="card card-body gap-3" method="post" action="/m/ { attr ( management_slug ) } /password">
<input type="hidden" name="csrf" value=" { attr ( csrf_token ) } ">
{ current_password_field }
<div>
<label class="form-label" for="dashboard-password"> { escape ( password_action ) } </label>
<input class="form-control" id="dashboard-password" name="dashboard_password" type="password" autocomplete="new-password" minlength="8" required>
</div>
<div>
<label class="form-label" for="dashboard-password-confirm">Confirm password</label>
<input class="form-control" id="dashboard-password-confirm" name="dashboard_password_confirm" type="password" autocomplete="new-password" minlength="8" required>
</div>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-primary rounded-pill" type="submit"> { escape ( password_action ) } </button>
</div>
</form>
{ remove_form }
</div>
</div>
</div>
</section>
"""
)
def _render_management_unlock_page (
management_slug : str ,
account : dict [ str , str | int | None ],
csrf_token : str ,
message : str | None = None ,
) -> str :
hostname = escape ( account [ "hostname" ])
message_html = ""
if message :
message_html = f '<div class="alert alert-warning" role="alert"> { escape ( message ) } </div>'
return _page (
"Unlock KarlDNS management" ,
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">Dashboard locked</p>
<h1> { hostname } </h1>
2026-05-21 10:05:40 +02:00
<p class="lead">Enter the dashboard password for this hostname.</p>
2026-05-20 23:55:27 +02:00
</div>
</section>
<section class="section">
<div class="container narrow">
{ message_html }
<form class="card card-body gap-3" method="post" action="/m/ { attr ( management_slug ) } /unlock">
<input type="hidden" name="csrf" value=" { attr ( csrf_token ) } ">
<div>
<label class="form-label" for="dashboard-unlock-password">Dashboard password</label>
<input class="form-control" id="dashboard-unlock-password" name="dashboard_password" type="password" autocomplete="current-password" required autofocus>
</div>
<label class="form-check d-flex align-items-center gap-2">
<input class="form-check-input" type="checkbox" name="stay_unlocked" value="1">
2026-05-21 10:05:40 +02:00
<span class="form-check-label">Remember this browser for 30 days</span>
2026-05-20 23:55:27 +02:00
</label>
2026-05-21 10:05:40 +02:00
<button class="btn btn-primary rounded-pill" type="submit">Unlock</button>
2026-05-20 23:55:27 +02:00
</form>
</div>
</section>
</main>
""" ),
)
def _user_event_row ( event : dict [ str , str | int | None ]) -> str :
created = escape ( event . get ( "created_at" ) or "-" )
2026-05-21 17:23:08 +02:00
status = escape ( _event_label ( event . get ( "status" )))
2026-05-20 23:55:27 +02:00
ipv4 = escape ( event . get ( "ipv4" ) or "-" )
ipv6 = escape ( event . get ( "ipv6" ) or "-" )
count = escape ( event . get ( "count" ) or 1 )
detail = escape ( event . get ( "detail" ) or "-" )
return f "<tr><td> { created } </td><td> { status } </td><td> { ipv4 } </td><td> { ipv6 } </td><td> { count } </td><td> { detail } </td></tr>"