from __future__ import annotations import sqlite3 from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from starlette.concurrency import run_in_threadpool from ..ddns_security import client_ip from ..ddns_service import normalize_domain from ..dependencies import ServiceDep, SettingsDep from ..web.forms import first_form_value, request_form from ..web.templates_public import _render_privacy_page, _render_public_home router = APIRouter() @router.get("/", response_class=HTMLResponse, include_in_schema=False) def index(settings: SettingsDep, service: ServiceDep) -> str: return _render_public_home(service, settings, None, None) @router.get("/magic", include_in_schema=False) def magic_form() -> RedirectResponse: return RedirectResponse("/#create", status_code=303) @router.post("/magic", response_class=HTMLResponse, include_in_schema=False) async def magic_account(request: Request, settings: SettingsDep, service: ServiceDep) -> str: await request_form(request, settings) account = await run_in_threadpool(service.create_managed_account, None) return _render_public_home(service, settings, account, None) @router.post("/request-domain", response_class=HTMLResponse, include_in_schema=False) async def request_domain(request: Request, settings: SettingsDep, service: ServiceDep) -> str: form = await request_form(request, settings) try: challenge = await run_in_threadpool( service.create_domain_challenge, first_form_value(form, "domain"), ) except sqlite3.IntegrityError as exc: raise HTTPException(status_code=409, detail="domain is already verified") from exc return _render_public_home(service, settings, None, None, challenge) @router.post("/verify-domain", response_class=HTMLResponse, include_in_schema=False) async def verify_domain(request: Request, settings: SettingsDep, service: ServiceDep) -> str: form = await request_form(request, settings) domain = normalize_domain(first_form_value(form, "domain")) claim_secret = first_form_value(form, "claim_secret") found, challenge = await run_in_threadpool(service.verify_domain, domain, claim_secret) if not found: message = "TXT record not found yet. DNS propagation can take a few minutes." return _render_public_home(service, settings, None, message, challenge) return _render_public_home(service, settings, None, "Domain verified. You can now create a hostname under it.", challenge) @router.post("/accounts", response_class=HTMLResponse, include_in_schema=False) async def self_service_account(request: Request, settings: SettingsDep, service: ServiceDep) -> str: form = await request_form(request, settings) mode = first_form_value(form, "mode") if mode == "managed": account = await run_in_threadpool(service.create_managed_account, None) else: claim_secret = first_form_value(form, "claim_secret") account = await run_in_threadpool( service.create_custom_account, first_form_value(form, "hostname"), claim_secret, None, ) return _render_public_home(service, settings, account, None) @router.get("/healthz", response_class=PlainTextResponse) def healthz() -> str: return "ok" @router.get("/ip", response_class=PlainTextResponse) def visible_ip(request: Request, settings: SettingsDep) -> str: return client_ip(request, settings) @router.get("/privacy", response_class=HTMLResponse, include_in_schema=False) def privacy() -> str: return _render_privacy_page()