2026-05-20 23:55:27 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
2026-05-21 10:05:40 +02:00
|
|
|
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse, Response
|
2026-05-20 23:55:27 +02:00
|
|
|
from starlette.concurrency import run_in_threadpool
|
|
|
|
|
|
|
|
|
|
from ..ddns_security import client_ip
|
|
|
|
|
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("/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")
|
2026-05-21 14:18:39 +02:00
|
|
|
if mode and mode != "managed":
|
|
|
|
|
raise HTTPException(status_code=400, detail="custom hostnames are not supported; create a KarlDNS hostname and use a CNAME")
|
|
|
|
|
if first_form_value(form, "hostname") or first_form_value(form, "claim_secret"):
|
|
|
|
|
raise HTTPException(status_code=400, detail="custom hostnames are not supported; create a KarlDNS hostname and use a CNAME")
|
|
|
|
|
account = await run_in_threadpool(service.create_managed_account, None)
|
2026-05-20 23:55:27 +02:00
|
|
|
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)
|
2026-05-21 10:05:40 +02:00
|
|
|
def privacy(settings: SettingsDep) -> str:
|
|
|
|
|
return _render_privacy_page(settings)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/robots.txt", response_class=PlainTextResponse, include_in_schema=False)
|
|
|
|
|
def robots_txt(settings: SettingsDep) -> str:
|
|
|
|
|
base_url = settings.public_base_url.rstrip("/")
|
|
|
|
|
admin_path = settings.admin_path.rstrip("/")
|
|
|
|
|
return "\n".join(
|
|
|
|
|
[
|
|
|
|
|
"User-agent: *",
|
|
|
|
|
f"Disallow: {admin_path}/",
|
|
|
|
|
"Disallow: /m/",
|
|
|
|
|
"Disallow: /u/",
|
|
|
|
|
"Disallow: /nic/",
|
|
|
|
|
"Disallow: /api/",
|
|
|
|
|
"Disallow: /docs",
|
|
|
|
|
"Disallow: /redoc",
|
|
|
|
|
"Disallow: /openapi.json",
|
|
|
|
|
f"Sitemap: {base_url}/sitemap.xml",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/sitemap.xml", include_in_schema=False)
|
|
|
|
|
def sitemap_xml(settings: SettingsDep) -> Response:
|
|
|
|
|
base_url = settings.public_base_url.rstrip("/")
|
|
|
|
|
body = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
|
|
|
<url>
|
|
|
|
|
<loc>{base_url}/</loc>
|
|
|
|
|
<changefreq>weekly</changefreq>
|
|
|
|
|
<priority>1.0</priority>
|
|
|
|
|
</url>
|
|
|
|
|
<url>
|
|
|
|
|
<loc>{base_url}/privacy</loc>
|
|
|
|
|
<changefreq>monthly</changefreq>
|
|
|
|
|
<priority>0.3</priority>
|
|
|
|
|
</url>
|
|
|
|
|
</urlset>
|
|
|
|
|
"""
|
|
|
|
|
return Response(body, media_type="application/xml")
|