from __future__ import annotations from typing import NoReturn from fastapi import APIRouter, HTTPException, Request, Response from fastapi.responses import HTMLResponse, PlainTextResponse from starlette.concurrency import run_in_threadpool from ..ddns_dns import DnsProviderError from ..ddns_security import client_ip, dashboard_csrf_token, require_dashboard_csrf from ..dependencies import ServiceDep, SettingsDep, StoreDep from ..event_statuses import ( DASHBOARD_HOSTNAME_DELETED, DASHBOARD_ROUTER_CREDENTIALS_ROTATED, DASHBOARD_UPDATE_URL_ROTATED, ) from ..web.forms import first_form_value, request_form from ..web.html_utils import trusted from ..web.templates import _page from ..web.templates_management import _render_management_page, _render_management_unlock_page from . import management_domains, management_passwords from .management_shared import ( get_active_management_account, get_active_management_account_async, has_management_access, locked_management_response, render_management_page_async, ) router = APIRouter() router.include_router(management_passwords.router) router.include_router(management_domains.router) @router.get("/m/{management_slug}", response_class=HTMLResponse, include_in_schema=False) def manage_magic(management_slug: str, request: Request, settings: SettingsDep, store: StoreDep, service: ServiceDep) -> str: account = get_active_management_account(store, management_slug) csrf_token = dashboard_csrf_token(management_slug, account) if not has_management_access(request, management_slug, account): return _render_management_unlock_page(management_slug, account, csrf_token) return _render_management_page(service, settings, management_slug, account, csrf_token) @router.post("/m/{management_slug}/rotate", response_class=HTMLResponse, include_in_schema=False) async def rotate_magic_credentials( management_slug: str, request: Request, settings: SettingsDep, store: StoreDep, service: ServiceDep, ) -> str: account = await get_active_management_account_async(store, management_slug) if not has_management_access(request, management_slug, account): return locked_management_response(management_slug, account) form = await request_form(request, settings) require_dashboard_csrf(management_slug, account, first_form_value(form, "csrf")) hostname = str(account["hostname"]) action = first_form_value(form, "action") if action == "update": rotated = await run_in_threadpool(store.rotate_update_slug, hostname) if not rotated: return _not_found() await run_in_threadpool( store.log_update_event, hostname, None, None, DASHBOARD_UPDATE_URL_ROTATED, "router update URL rotated", client_ip(request, settings), ) return await render_management_page_async( service, settings, management_slug, rotated, dashboard_csrf_token(management_slug, rotated), message="Update-URL rotated.", ) if action == "password": rotated = await run_in_threadpool(store.rotate_password, hostname) if not rotated: return _not_found() rotated["management_slug"] = management_slug await run_in_threadpool( store.log_update_event, hostname, None, None, DASHBOARD_ROUTER_CREDENTIALS_ROTATED, "router credentials rotated", client_ip(request, settings), ) return await render_management_page_async( service, settings, management_slug, rotated, dashboard_csrf_token(management_slug, rotated), message="Router credentials rotated. Copy the new password now.", created_account=rotated, ) raise _bad_rotation_action() @router.post("/m/{management_slug}/delete", include_in_schema=False) async def delete_magic(management_slug: str, request: Request, settings: SettingsDep, store: StoreDep, service: ServiceDep) -> Response: account = await get_active_management_account_async(store, management_slug) if not has_management_access(request, management_slug, account): return HTMLResponse(_render_management_unlock_page(management_slug, account, dashboard_csrf_token(management_slug, account)), status_code=401) form = await request_form(request, settings) require_dashboard_csrf(management_slug, account, first_form_value(form, "csrf")) hostname = str(account["hostname"]) try: await run_in_threadpool(service.delete_account, hostname, "management-link") except DnsProviderError: return PlainTextResponse("DNS delete failed; hostname was not deleted", status_code=500) await run_in_threadpool( store.log_update_event, hostname, None, None, DASHBOARD_HOSTNAME_DELETED, "hostname deleted from dashboard", client_ip(request, settings), ) return HTMLResponse( _page( "DynDNS deleted", trusted( """

Deleted

Hostname deleted.

The update URL no longer works.

Home
""" ), ) ) def _not_found() -> NoReturn: raise HTTPException(status_code=404, detail="not found") def _bad_rotation_action() -> NoReturn: raise HTTPException(status_code=400, detail="invalid rotation action")