Archived
Simplify setup flow and public API docs
This commit is contained in:
1 parent
2e401124e8
commit
97a76aa91e
12 files changed
+119
-140
No files matched your search
@@ -41,10 +41,6 @@ OPENAPI_TAGS = [
|
||||
"name": "router",
|
||||
"description": "Router-facing registration and IP update endpoints for simple URL clients.",
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Operator API endpoints protected by the same admin HTTP Basic auth as the web admin console.",
|
||||
},
|
||||
]
|
||||
|
||||
CLEANUP_TASK_NAME = "cleanup"
|
||||
|
||||
@@ -127,6 +127,7 @@ async def delete_hostname(dashboard_secret: ManagementSlugPath, store: StoreDep,
|
||||
summary="Read operator dashboard summary",
|
||||
description="Requires admin HTTP Basic auth. Returns the same high-level data shown in the operator console.",
|
||||
tags=["admin"],
|
||||
include_in_schema=False,
|
||||
responses={401: {"model": ErrorResponse}},
|
||||
)
|
||||
async def read_admin(
|
||||
@@ -161,6 +162,7 @@ async def read_admin(
|
||||
summary="List operator hostnames",
|
||||
description="Requires admin HTTP Basic auth.",
|
||||
tags=["admin"],
|
||||
include_in_schema=False,
|
||||
responses={401: {"model": ErrorResponse}},
|
||||
)
|
||||
async def list_admin_hostnames(
|
||||
@@ -185,6 +187,7 @@ async def list_admin_hostnames(
|
||||
summary="List operator update history",
|
||||
description="Requires admin HTTP Basic auth. Supports offset pagination or keyset pagination with before.",
|
||||
tags=["admin"],
|
||||
include_in_schema=False,
|
||||
responses={401: {"model": ErrorResponse}},
|
||||
)
|
||||
async def list_admin_events(
|
||||
@@ -210,6 +213,7 @@ async def list_admin_events(
|
||||
summary="List operator DNS records",
|
||||
description="Requires admin HTTP Basic auth.",
|
||||
tags=["admin"],
|
||||
include_in_schema=False,
|
||||
responses={401: {"model": ErrorResponse}},
|
||||
)
|
||||
async def list_admin_records(
|
||||
|
||||
@@ -50,7 +50,7 @@ def manage_dashboard(
|
||||
account = get_active_management_account(store, management_slug)
|
||||
csrf_token = dashboard_csrf_token(management_slug, account)
|
||||
if not management_account_is_accepted(account):
|
||||
return _render_pending_management_page(settings, management_slug, account, csrf_token)
|
||||
return _render_pending_management_page(service, settings, management_slug, account, csrf_token, request_ip=client_ip(request, settings))
|
||||
if not has_management_access(request, settings, management_slug, account):
|
||||
return _render_management_unlock_page(management_slug, account, csrf_token)
|
||||
message = "Hostname registered. Add the update URL to your router." if activated else None
|
||||
@@ -79,22 +79,26 @@ async def activate_dashboard(
|
||||
except BadIpError:
|
||||
return HTMLResponse(
|
||||
_render_pending_management_page(
|
||||
service,
|
||||
settings,
|
||||
management_slug,
|
||||
account,
|
||||
dashboard_csrf_token(management_slug, account),
|
||||
"Registration needs a public source IP. Try again from your home connection or use the API with myip.",
|
||||
client_ip(request, settings),
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
except DnsProviderError:
|
||||
return HTMLResponse(
|
||||
_render_pending_management_page(
|
||||
service,
|
||||
settings,
|
||||
management_slug,
|
||||
account,
|
||||
dashboard_csrf_token(management_slug, account),
|
||||
"DNS registration failed. The hostname was not registered.",
|
||||
client_ip(request, settings),
|
||||
),
|
||||
status_code=500,
|
||||
)
|
||||
@@ -124,7 +128,15 @@ async def rotate_dashboard_secrets(
|
||||
account = await get_active_management_account_async(store, management_slug)
|
||||
if not management_account_is_accepted(account):
|
||||
return HTMLResponse(
|
||||
_render_pending_management_page(settings, management_slug, account, dashboard_csrf_token(management_slug, account), "Register this hostname first."),
|
||||
_render_pending_management_page(
|
||||
service,
|
||||
settings,
|
||||
management_slug,
|
||||
account,
|
||||
dashboard_csrf_token(management_slug, account),
|
||||
"Register this hostname first.",
|
||||
client_ip(request, settings),
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
if not has_management_access(request, settings, management_slug, account):
|
||||
|
||||
@@ -92,7 +92,6 @@ async def render_management_page_async(
|
||||
account: ManagementAccount,
|
||||
csrf_token: str,
|
||||
message: str | None = None,
|
||||
created_account: dict[str, str] | None = None,
|
||||
request_ip: str | None = None,
|
||||
) -> str:
|
||||
return await run_in_threadpool(
|
||||
@@ -103,7 +102,6 @@ async def render_management_page_async(
|
||||
account,
|
||||
csrf_token,
|
||||
message,
|
||||
created_account,
|
||||
request_ip,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse, Response
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from ..dependencies import ServiceDep, SettingsDep
|
||||
@@ -13,15 +13,15 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
def index(request: Request, settings: SettingsDep, service: ServiceDep) -> str:
|
||||
return _render_public_home(service, settings, None, None, client_ip(request, settings))
|
||||
def index(request: Request, settings: SettingsDep) -> str:
|
||||
return _render_public_home(settings, None, client_ip(request, settings))
|
||||
|
||||
|
||||
@router.post("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def create_hostname(request: Request, settings: SettingsDep, service: ServiceDep) -> str:
|
||||
async def create_hostname(request: Request, settings: SettingsDep, service: ServiceDep) -> RedirectResponse:
|
||||
await request_form(request, settings)
|
||||
account = await run_in_threadpool(service.create_managed_account)
|
||||
return _render_public_home(service, settings, account, None, client_ip(request, settings))
|
||||
return RedirectResponse(service.dashboard_url(account), status_code=303)
|
||||
|
||||
|
||||
@router.get("/healthz", response_class=PlainTextResponse, include_in_schema=False)
|
||||
|
||||
@@ -28,7 +28,7 @@ def _event_label(status: object) -> str:
|
||||
return _EVENT_LABELS.get(value, value.replace("_", " "))
|
||||
|
||||
|
||||
def _copy_row(label: str, value: str, sensitivity: str | None = None) -> SafeHtml:
|
||||
def _copy_row(label: str, value: str, sensitivity: str | None = None, *, mask_secret: bool = True) -> SafeHtml:
|
||||
safe_label = escape(label)
|
||||
safe_value = escape(value)
|
||||
badge = ""
|
||||
@@ -37,14 +37,15 @@ def _copy_row(label: str, value: str, sensitivity: str | None = None) -> SafeHtm
|
||||
safe_sensitivity = escape(sensitivity)
|
||||
badge_class = "text-bg-warning" if is_secret else "text-bg-secondary"
|
||||
badge = f'<span class="badge {badge_class} rounded-pill">{safe_sensitivity}</span>'
|
||||
should_mask = is_secret and mask_secret
|
||||
value_field = (
|
||||
f'<input class="form-control copy-value text-truncate" type="password" value="{safe_value}" readonly data-secret-input>'
|
||||
if is_secret
|
||||
if should_mask
|
||||
else f'<code class="form-control copy-value text-truncate">{safe_value}</code>'
|
||||
)
|
||||
reveal_button = (
|
||||
f'<button type="button" class="btn btn-outline-secondary" data-secret-toggle aria-label="Show {safe_label}">View</button>'
|
||||
if is_secret
|
||||
if should_mask
|
||||
else ""
|
||||
)
|
||||
return trusted(
|
||||
@@ -194,16 +195,6 @@ def _secret_notice(*, include_registration: bool = False) -> SafeHtml:
|
||||
)
|
||||
|
||||
|
||||
def _one_time_secret_notice() -> SafeHtml:
|
||||
return trusted(
|
||||
"""
|
||||
<div class="alert alert-warning my-3 py-2 small" role="alert">
|
||||
Copy the private URLs now. Newly created or rotated secrets are shown only once.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _message_band(message: str | None) -> SafeHtml:
|
||||
if not message:
|
||||
return trusted("")
|
||||
|
||||
@@ -3,9 +3,11 @@ from __future__ import annotations
|
||||
from ..services.ddns import DdnsService
|
||||
from ..settings import DdnsSettings
|
||||
from .components import (
|
||||
_code_window,
|
||||
_copy_menu,
|
||||
_copy_row,
|
||||
_event_label,
|
||||
_example_update_ips,
|
||||
_fritzbox_fields,
|
||||
_manual_update_commands,
|
||||
_manual_update_panel,
|
||||
@@ -13,7 +15,6 @@ from .components import (
|
||||
)
|
||||
from .html_utils import SafeHtml, attr, escape, trusted
|
||||
from .layout import _page, _top_nav
|
||||
from .templates_public_sections import _created_hostname_panel
|
||||
|
||||
|
||||
def _render_management_page(
|
||||
@@ -23,7 +24,6 @@ def _render_management_page(
|
||||
account: dict[str, str | int | None],
|
||||
csrf_token: str,
|
||||
message: str | None = None,
|
||||
created_account: dict[str, str] | None = None,
|
||||
request_ip: str | None = None,
|
||||
) -> str:
|
||||
account_for_url = {
|
||||
@@ -43,7 +43,6 @@ def _render_management_page(
|
||||
<tr><td colspan="6" class="empty">No router updates yet.</td></tr>
|
||||
"""
|
||||
message_html = _management_message(message)
|
||||
rotated_links = _created_hostname_panel(service, created_account, request_ip)
|
||||
dashboard_password_panel = _dashboard_password_panel(management_slug, csrf_token, bool(account.get("dashboard_password_hash")))
|
||||
return _page(
|
||||
"KarlDNS management",
|
||||
@@ -59,7 +58,6 @@ def _render_management_page(
|
||||
</div>
|
||||
</section>
|
||||
{message_html}
|
||||
{rotated_links}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="row g-4 align-items-start">
|
||||
@@ -149,18 +147,31 @@ def _render_management_page(
|
||||
)
|
||||
|
||||
def _render_pending_management_page(
|
||||
service: DdnsService,
|
||||
settings: DdnsSettings,
|
||||
management_slug: str,
|
||||
account: dict[str, str | int | None],
|
||||
csrf_token: str,
|
||||
message: str | None = None,
|
||||
request_ip: str | None = None,
|
||||
) -> str:
|
||||
hostname = escape(account["hostname"])
|
||||
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)
|
||||
registration_url = service.registration_url({"management_slug": management_slug})
|
||||
dashboard_url = service.dashboard_url({"management_slug": management_slug})
|
||||
hostname = escape(raw_hostname)
|
||||
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"
|
||||
manual_commands = _manual_update_commands(update_url, request_ip)
|
||||
example_ipv4, example_ipv6 = _example_update_ips(request_ip)
|
||||
return _page(
|
||||
"Register hostname",
|
||||
"KarlDNS setup",
|
||||
trusted(f"""
|
||||
<main id="main">
|
||||
{_top_nav()}
|
||||
@@ -168,7 +179,7 @@ def _render_pending_management_page(
|
||||
<div class="container text-center d-grid justify-items-center gap-3">
|
||||
<p class="eyebrow">Pending setup</p>
|
||||
<h1>{hostname}</h1>
|
||||
<p class="lead">Register this hostname after you have copied the private values. Unregistered setups are cleaned up after {escape(cleanup_text)}.</p>
|
||||
<p class="lead">Save these values, then register the hostname. Pending setups are cleaned up after {escape(cleanup_text)}.</p>
|
||||
<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>
|
||||
@@ -176,9 +187,44 @@ def _render_pending_management_page(
|
||||
</div>
|
||||
</section>
|
||||
{message_html}
|
||||
<section class="section">
|
||||
<div class="container narrow d-grid gap-4">
|
||||
<div>
|
||||
<p class="eyebrow">Save</p>
|
||||
<h2>Your values.</h2>
|
||||
<p class="section-copy">This page is private. You can revisit it while the setup is pending. After registration, the dashboard hides secrets by default.</p>
|
||||
<p class="section-copy"><strong>Public:</strong> hostname and CNAME target. <strong>Secret:</strong> update URL, register URL, and private dashboard.</p>
|
||||
</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("Register URL:", registration_url, "Secret", mask_secret=False)}
|
||||
{_copy_row("Private dashboard:", dashboard_url, "Secret", mask_secret=False)}
|
||||
{_copy_row("CNAME target:", raw_hostname, "Public")}
|
||||
</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>
|
||||
<section class="section section-compact">
|
||||
<div class="container narrow text-center">
|
||||
<p class="section-copy mx-auto">Lost the first page? Create a new hostname. Private URLs are shown only once.</p>
|
||||
<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>
|
||||
<p class="section-copy">Scripts can send the IP explicitly, or omit it and let KarlDNS use the request IP. Your current public examples use <code>{escape(example_ipv4)}</code> and <code>{escape(example_ipv6)}</code> where available.</p>
|
||||
{_code_window(manual_commands, copy_label="manual update commands", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Use your own DNS name.</h2>
|
||||
<p class="section-copy">Create a CNAME at your DNS provider that points to <strong>{hostname}</strong>. KarlDNS only updates this generated hostname.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..services.ddns import DdnsService
|
||||
from ..settings import DdnsSettings
|
||||
from .components import _code_window, _example_update_ips, _message_band
|
||||
from .html_utils import escape, trusted
|
||||
from .layout import _page, _top_nav
|
||||
from .templates_public_sections import _created_hostname_panel, _linux_installer_section
|
||||
from .templates_public_sections import _linux_installer_section
|
||||
|
||||
|
||||
def _render_public_home(
|
||||
service: DdnsService,
|
||||
settings: DdnsSettings,
|
||||
created_account: dict[str, str] | None,
|
||||
message: str | None,
|
||||
request_ip: str | None = None,
|
||||
) -> str:
|
||||
@@ -30,7 +27,6 @@ def _render_public_home(
|
||||
f"curl -sS '{base_url}/api/v1/router/update/<secret>'",
|
||||
]
|
||||
)
|
||||
created_html = _created_hostname_panel(service, created_account, request_ip)
|
||||
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."
|
||||
return _page(
|
||||
@@ -55,7 +51,6 @@ def _render_public_home(
|
||||
</div>
|
||||
</section>
|
||||
{message_html}
|
||||
{created_html}
|
||||
<section class="section section-compact">
|
||||
<div class="container narrow text-center">
|
||||
<p class="eyebrow">How it works</p>
|
||||
@@ -169,8 +164,6 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
|
||||
status_example = f"curl -sS '{base_url}/api/v1/hostnames/<dashboard-secret>?history_limit=10'"
|
||||
rotate_example = f"curl -sS -X POST {base_url}/api/v1/hostnames/<dashboard-secret>/rotate-update-url"
|
||||
schema_example = f"curl -sS {base_url}/openapi.json"
|
||||
admin_dashboard_example = f"curl -sS -u admin:<admin-password> {base_url}/api/v1/admin/dashboard"
|
||||
admin_history_example = f"curl -sS -u admin:<admin-password> '{base_url}/api/v1/admin/events?limit=25'"
|
||||
description = "Small KarlDNS API guide for creating, activating, updating, reading, and deleting generated Dynamic DNS hostnames."
|
||||
return _page(
|
||||
"KarlDNS API",
|
||||
@@ -181,7 +174,7 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
|
||||
<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">Three surfaces: admin API, user dashboard data, and router registration/update calls.</p>
|
||||
<p class="lead">Create a hostname, register it, update it, and read its status.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section section-compact">
|
||||
@@ -189,14 +182,12 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
|
||||
<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 one-time secrets. The response includes <code>registration_url</code>.</li>
|
||||
<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/<secret>.</strong> Register the hostname after secrets are saved. Uses the request IP, or <code>myip</code>/<code>myipv6</code>, for the initial DNS record.</li>
|
||||
<li><strong>GET or POST /api/v1/router/update/<secret>.</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/<secret>.</strong> Read hostname status and recent history.</li>
|
||||
<li><strong>POST /api/v1/hostnames/<secret>/rotate-update-url.</strong> Replace the private router update URL.</li>
|
||||
<li><strong>DELETE /api/v1/hostnames/<secret>.</strong> Delete hostname and DNS records.</li>
|
||||
<li><strong>GET /api/v1/admin/dashboard.</strong> Read operator summary, metrics, recent hostnames, cleanup state, and recent update history with admin Basic auth.</li>
|
||||
<li><strong>GET /api/v1/admin/events.</strong> Read paginated update history with admin Basic auth. Also available: <code>/api/v1/admin/hostnames</code> and <code>/api/v1/admin/records</code>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
@@ -223,15 +214,9 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
|
||||
</div>
|
||||
<div>
|
||||
<h2>Schema</h2>
|
||||
<p class="section-copy">Machine-readable OpenAPI is still available. Operator API routes are included and require admin Basic auth.</p>
|
||||
<p class="section-copy">Machine-readable OpenAPI is available for the public hostname and router endpoints.</p>
|
||||
{_code_window(schema_example, copy_label="schema command", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Operator API</h2>
|
||||
<p class="section-copy">Admin endpoints use the same HTTP Basic auth as the operator console.</p>
|
||||
{_code_window(admin_dashboard_example, copy_label="admin dashboard command", class_name="mt-3")}
|
||||
{_code_window(admin_history_example, copy_label="admin history command", class_name="mt-3")}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..security import dashboard_csrf_token
|
||||
from ..services.ddns import DdnsService
|
||||
from .components import (
|
||||
_code_window,
|
||||
_copy_menu,
|
||||
_copy_row,
|
||||
_fritzbox_fields,
|
||||
_manual_update_commands,
|
||||
_manual_update_panel,
|
||||
_one_time_secret_notice,
|
||||
_secret_notice,
|
||||
)
|
||||
from .html_utils import SafeHtml, attr, escape, trusted
|
||||
from .components import _code_window
|
||||
from .html_utils import SafeHtml, trusted
|
||||
|
||||
|
||||
def _linux_installer_section(base_url: str) -> SafeHtml:
|
||||
@@ -44,45 +33,3 @@ def _linux_installer_section(base_url: str) -> SafeHtml:
|
||||
</section>
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _created_hostname_panel(service: DdnsService, created_account: dict[str, str] | None, request_ip: str | None = None) -> SafeHtml:
|
||||
if not created_account:
|
||||
return trusted("")
|
||||
hostname = created_account["hostname"]
|
||||
update_url = service.fritz_update_url(created_account)
|
||||
management_url = service.dashboard_url(created_account)
|
||||
registration_url = service.registration_url(created_account)
|
||||
csrf_token = dashboard_csrf_token(created_account["management_slug"], created_account)
|
||||
management_slug = attr(created_account["management_slug"])
|
||||
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
|
||||
fritzbox_fields = _fritzbox_fields(update_url, hostname, management_url)
|
||||
manual_commands = _manual_update_commands(update_url, request_ip)
|
||||
return trusted(
|
||||
f"""
|
||||
<section class="section success-section">
|
||||
<div class="container">
|
||||
<p class="eyebrow">Pending setup</p>
|
||||
<h2>{escape(hostname)}</h2>
|
||||
<p class="section-copy">Copy these values now. Then register the hostname so router updates can start. Unregistered setups are removed after 24 hours.</p>
|
||||
{_one_time_secret_notice()}
|
||||
{_secret_notice(include_registration=True)}
|
||||
<div class="copy-actions my-3">
|
||||
{_copy_menu("Copy setup values", [("Router fields", fritzbox_fields), ("Register URL", registration_url), ("CNAME", cname_record)])}
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
{_copy_row("Update-URL:", update_url, "Secret")}
|
||||
{_copy_row("Register URL:", registration_url, "Secret")}
|
||||
{_copy_row("Domainnamen:", hostname, "Public")}
|
||||
{_copy_row("CNAME target:", hostname, "Public")}
|
||||
{_copy_row("Private dashboard:", management_url, "Secret")}
|
||||
</div>
|
||||
<form class="mt-4" method="post" action="/m/{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>
|
||||
{_manual_update_panel(manual_commands)}
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
)
|
||||
Reference in new issue
Block a user