Remove admin credential generation flow

This commit is contained in:
StasonJatham committed 2026-05-20 18:52:51 +02:00
1 parent 033b8564dd
commit 59c7618d15
2 files changed
+9 -74

No files matched your search

+2 -67
View File
@@ -334,34 +334,6 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
store.list_cleanup_runs(), store.list_cleanup_runs(),
store.list_update_events(25), store.list_update_events(25),
None, None,
None,
)
@app.post("/admin/accounts", include_in_schema=False)
async def admin_create_account(request: Request, _: None = Depends(authenticate_admin)) -> Response:
form = await _request_form(request, settings)
require_admin_csrf(settings, _first_form_value(form, "csrf"))
hostname = normalize_hostname(_first_form_value(form, "hostname"), settings)
username = _first_form_value(form, "username") or None
if not hostname:
raise HTTPException(status_code=400, detail="hostname is required")
if settings.allowed_hosts and hostname not in settings.allowed_hosts:
raise HTTPException(status_code=403, detail="hostname is not allowed")
try:
account = store.create_account(hostname, username)
except sqlite3.IntegrityError as exc:
raise HTTPException(status_code=409, detail="hostname already exists") from exc
return HTMLResponse(
_render_admin_page(
service,
settings,
store.list_accounts(),
store.list_domain_challenges(),
store.list_cleanup_runs(),
store.list_update_events(25),
account,
None,
)
) )
@app.post("/admin/accounts/delete", include_in_schema=False) @app.post("/admin/accounts/delete", include_in_schema=False)
@@ -414,7 +386,6 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
store.list_domain_challenges(), store.list_domain_challenges(),
store.list_cleanup_runs(), store.list_cleanup_runs(),
store.list_update_events(25), store.list_update_events(25),
None,
notice, notice,
) )
) )
@@ -439,7 +410,6 @@ def make_app(settings: DdnsSettings | None = None) -> FastAPI:
store.list_domain_challenges(), store.list_domain_challenges(),
store.list_cleanup_runs(), store.list_cleanup_runs(),
store.list_update_events(25), store.list_update_events(25),
None,
notice, notice,
) )
) )
@@ -769,10 +739,6 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
""" """
def _created_account_panel(service: DdnsService, created_account: dict[str, str] | None) -> str:
return _credentials_panel(service, created_account)
def _admin_secret_panel(title: str, rows: list[str]) -> str: def _admin_secret_panel(title: str, rows: list[str]) -> str:
return f""" return f"""
<section class="section success-section"> <section class="section success-section">
@@ -913,7 +879,6 @@ def _render_admin_page(
domain_challenges: list[dict[str, str | None]], domain_challenges: list[dict[str, str | None]],
cleanup_runs: list[dict[str, str | int]], cleanup_runs: list[dict[str, str | int]],
update_events: list[dict[str, str | None]], update_events: list[dict[str, str | None]],
created_account: dict[str, str] | None,
notice_html: str | None, notice_html: str | None,
) -> str: ) -> str:
csrf = html.escape(admin_csrf_token(settings)) csrf = html.escape(admin_csrf_token(settings))
@@ -929,11 +894,6 @@ def _render_admin_page(
event_rows = "\n".join(_event_row(event) for event in update_events) or """ event_rows = "\n".join(_event_row(event) for event in update_events) or """
<tr><td colspan="6" class="empty">No update events yet.</td></tr> <tr><td colspan="6" class="empty">No update events yet.</td></tr>
""" """
suffix_help = (
f"Short names are expanded under {html.escape(settings.hostname_suffix)}."
if settings.hostname_suffix
else "Use a full hostname, for example home.example.net."
)
return _page( return _page(
"DynDNS Admin", "DynDNS Admin",
f""" f"""
@@ -942,12 +902,11 @@ def _render_admin_page(
<section class="section admin-heading"> <section class="section admin-heading">
<div class="container"> <div class="container">
<p class="eyebrow">DynDNS aktiv</p> <p class="eyebrow">DynDNS aktiv</p>
<h1>Router credentials</h1> <h1>Operator console</h1>
<p class="lead">Create, inspect, and retire router update credentials.</p> <p class="lead">Inspect hostnames, verify domain claims, rotate links, run cleanup, and review router update events.</p>
</div> </div>
</section> </section>
{_created_account_panel(service, created_account)}
{notice_html or ""} {notice_html or ""}
<section class="section"> <section class="section">
@@ -966,30 +925,6 @@ def _render_admin_page(
</div> </div>
</section> </section>
<section class="section">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-7">
<p class="eyebrow">Generate credentials</p>
<h2>Router credentials</h2>
<p class="section-copy">Generated credentials produce the FRITZ!Box fields: Update-URL, Domainnamen, Benutzername, Kennwort.</p>
<p class="small text-secondary mb-0 mt-2">{suffix_help}</p>
</div>
<form method="post" action="/admin/accounts" class="col-lg-5 card card-body gap-3" aria-label="Create router credentials">
<input type="hidden" name="csrf" value="{csrf}">
<label>Domainnamen
<input class="form-control" name="hostname" placeholder="home.example.net" autocomplete="off" autocapitalize="none" spellcheck="false" required>
</label>
<label>Benutzername
<input class="form-control" name="username" placeholder="auto-generated" autocomplete="off">
<span class="form-text m-0">Leave empty to generate one from the hostname.</span>
</label>
<button type="submit" class="btn btn-primary rounded-pill">Generate credentials</button>
</form>
</div>
</div>
</section>
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row"> <div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
+7 -7
View File
@@ -152,7 +152,7 @@ def test_generated_account_cannot_update_other_hostname(tmp_path: Path) -> None:
assert response.status_code == 401 assert response.status_code == 401
def test_admin_page_shows_account_form(tmp_path: Path) -> None: def test_admin_page_shows_operator_views_without_create_form(tmp_path: Path) -> None:
database_path = tmp_path / "ddns.sqlite3" database_path = tmp_path / "ddns.sqlite3"
store = DdnsStore(database_path) store = DdnsStore(database_path)
store.create_domain_challenge("example.net") store.create_domain_challenge("example.net")
@@ -163,11 +163,11 @@ def test_admin_page_shows_account_form(tmp_path: Path) -> None:
assert response.status_code == 200 assert response.status_code == 200
assert response.headers["cache-control"] == "no-store" assert response.headers["cache-control"] == "no-store"
assert "Update-URL" in response.text assert "Operator console" in response.text
assert "Domainnamen" in response.text assert "Generate credentials" not in response.text
assert "Benutzername" in response.text assert 'action="/admin/accounts"' not in response.text
assert "Kennwort" in response.text
assert "Scheduled cleanup" in response.text assert "Scheduled cleanup" in response.text
assert "Active credentials" in response.text
assert "Domain claims" in response.text assert "Domain claims" in response.text
assert "Recent events" in response.text assert "Recent events" in response.text
assert "example.net" in response.text assert "example.net" in response.text
@@ -178,9 +178,9 @@ def test_admin_posts_require_csrf(tmp_path: Path) -> None:
client = TestClient(app) client = TestClient(app)
response = client.post( response = client.post(
"/admin/accounts", "/admin/cleanup",
auth=("admin", "admin"), auth=("admin", "admin"),
data={"hostname": "home.example.net"}, data={},
) )
assert response.status_code == 403 assert response.status_code == 403