from __future__ import annotations from fastapi import APIRouter, HTTPException, Request, Response, status from starlette.concurrency import run_in_threadpool from ..ddns_dns import DnsProviderError from ..ddns_service import BadIpError from ..dependencies import ServiceDep, StoreDep from ..schemas import ( ErrorResponse, HostnameCredentials, MagicHostnameRequest, ManagedRecord, RouterUpdateResponse, _credentials_response, _managed_record, ) from .path_params import ManagementSlugPath, UpdateSlugPath router = APIRouter(prefix="/api/v1") @router.post( "/hostnames/magic", response_model=HostnameCredentials, status_code=status.HTTP_201_CREATED, summary="Create a KarlDNS hostname", tags=["hostnames"], responses={500: {"model": ErrorResponse}}, ) async def create_magic_hostname(payload: MagicHostnameRequest, service: ServiceDep) -> HostnameCredentials: account = await run_in_threadpool(service.create_managed_account, payload.username) return _credentials_response(service, account) @router.get( "/management/{management_slug}", response_model=ManagedRecord, summary="Read a hostname by private dashboard link", description="Uses the private dashboard link secret. Dashboard passwords protect only the web dashboard, not this API endpoint.", tags=["management"], responses={404: {"model": ErrorResponse}}, ) async def read_managed_hostname(management_slug: ManagementSlugPath, store: StoreDep, service: ServiceDep) -> ManagedRecord: account = await run_in_threadpool(store.get_account_by_management_slug, management_slug) if not account or account["disabled"]: raise HTTPException(status_code=404, detail="not found") return _managed_record(service, account) @router.delete( "/management/{management_slug}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a hostname by private dashboard link", description="Uses the private dashboard link secret. Dashboard passwords protect only the web dashboard, not this API endpoint.", tags=["management"], responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, ) async def delete_managed_hostname(management_slug: ManagementSlugPath, store: StoreDep, service: ServiceDep) -> Response: account = await run_in_threadpool(store.get_account_by_management_slug, management_slug) if not account or account["disabled"]: raise HTTPException(status_code=404, detail="not found") hostname = str(account["hostname"]) try: await run_in_threadpool(service.delete_account, hostname, "api") except DnsProviderError as exc: raise HTTPException(status_code=500, detail="DNS delete failed. Hostname was not deleted.") from exc return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get( "/updates/{update_slug}", response_model=RouterUpdateResponse, summary="Router-compatible update endpoint", tags=["updates"], responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, ) async def update_hostname( update_slug: UpdateSlugPath, request: Request, store: StoreDep, service: ServiceDep, myip: str | None = None, ipaddr: str | None = None, myipv6: str | None = None, ip6addr: str | None = None, ) -> RouterUpdateResponse: account = await run_in_threadpool(store.get_account_by_slug, update_slug) if not account or account["disabled"]: raise HTTPException(status_code=404, detail="not found") hostname = str(account["hostname"]) try: outcome = await run_in_threadpool( service.apply_update, request, hostname, myip, ipaddr, myipv6, ip6addr, ) except BadIpError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except DnsProviderError as exc: raise HTTPException(status_code=500, detail="DNS update failed") from exc result = outcome.result return RouterUpdateResponse(status="good" if result.changed else "nochg", hostname=hostname, ip=result.ipv4 or result.ipv6 or "")