from __future__ import annotations from typing import NoReturn from fastapi import APIRouter, HTTPException, Request, Response from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from starlette.concurrency import run_in_threadpool from ..dependencies import ServiceDep, SettingsDep, StoreDep from ..dns import DnsProviderError from ..event_statuses import ( DASHBOARD_HOSTNAME_ACTIVATED, DASHBOARD_HOSTNAME_DELETED, DASHBOARD_UPDATE_URL_ROTATED, ) from ..security import client_ip, dashboard_csrf_token, require_dashboard_csrf from ..services.ddns import BadIpError from ..web.forms import first_form_value, request_form from ..web.html_utils import trusted from ..web.layout import _page from ..web.templates_management import ( _render_management_page, _render_management_unlock_page, _render_pending_management_page, ) from . import management_passwords from .management_shared import ( get_active_management_account, get_active_management_account_async, has_management_access, locked_management_response, management_account_is_accepted, render_management_page_async, ) from .path_params import ManagementSlugPath router = APIRouter() router.include_router(management_passwords.router) @router.get("/m/{management_slug}", response_class=HTMLResponse, include_in_schema=False) def manage_dashboard( management_slug: ManagementSlugPath, request: Request, settings: SettingsDep, store: StoreDep, service: ServiceDep, activated: bool = False, ) -> str: 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( 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 return _render_management_page( service, settings, management_slug, account, csrf_token, message=message, request_ip=client_ip(request, settings), ) @router.post("/m/{management_slug}/activate", include_in_schema=False, response_model=None) async def activate_dashboard( management_slug: ManagementSlugPath, request: Request, settings: SettingsDep, store: StoreDep, service: ServiceDep, myip: str | None = None, ipaddr: str | None = None, myipv6: str | None = None, ip6addr: str | None = None, ) -> RedirectResponse | HTMLResponse: account = await get_active_management_account_async(store, management_slug) form = await request_form(request, settings) require_dashboard_csrf(management_slug, account, first_form_value(form, "csrf")) hostname = str(account["hostname"]) if not management_account_is_accepted(account): try: await run_in_threadpool(service.publish_initial_dns, request, hostname, myip, ipaddr, myipv6, ip6addr) except BadIpError: return HTMLResponse( _render_pending_management_page( service, settings, management_slug, account, dashboard_csrf_token(management_slug, account), "The supplied IP address is invalid. Leave IP fields empty and let the router send the first update.", 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, ) activated_account = await run_in_threadpool(lambda: store.activate_account(hostname)) if not activated_account: return _not_found() await run_in_threadpool( store.log_update_event, hostname, None, None, DASHBOARD_HOSTNAME_ACTIVATED, "hostname registered", client_ip(request, settings), ) return RedirectResponse(f"/m/{management_slug}?activated=1", status_code=303) @router.post("/m/{management_slug}/rotate", response_class=HTMLResponse, include_in_schema=False, response_model=None) async def rotate_dashboard_secrets( management_slug: ManagementSlugPath, request: Request, settings: SettingsDep, store: StoreDep, service: ServiceDep, ) -> str | HTMLResponse: account = await get_active_management_account_async(store, management_slug) if not management_account_is_accepted(account): return HTMLResponse( _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): 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") request_ip = client_ip(request, settings) 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", request_ip, ) return await render_management_page_async( service, settings, management_slug, rotated, dashboard_csrf_token(management_slug, rotated), message="Update URL changed. Update your router now.", request_ip=request_ip, ) raise _bad_rotation_action() @router.post("/m/{management_slug}/delete", include_in_schema=False) async def delete_dashboard_hostname( management_slug: ManagementSlugPath, 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, settings, 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( "Hostname 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")