Archived
Refactor UI assets and harden quality checks
This commit is contained in:
1 parent
23ab466c52
commit
4ecc8eb874
62 files changed
+3314
-3206
No files matched your search
+136
-86
@@ -1,20 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from typing import Annotated
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from ..dependencies import ServiceDep, StoreDep, apply_update_response
|
||||
from ..dependencies import ServiceDep, apply_update_response
|
||||
from ..dns import DnsProviderError
|
||||
from ..event_statuses import DASHBOARD_HOSTNAME_ACTIVATED
|
||||
from ..security import client_ip
|
||||
from ..services.ddns import BadIpError
|
||||
from ..services.ddns import BadIpError, DdnsService, UpdateIpParams
|
||||
from .path_params import ManagementSlugPath, UpdateSlugPath
|
||||
|
||||
router = APIRouter()
|
||||
RouterIpQuery = Annotated[str | None, Query()]
|
||||
_IpParams = UpdateIpParams
|
||||
|
||||
|
||||
def _ip_params_from_query(
|
||||
myip: RouterIpQuery = None,
|
||||
ipaddr: RouterIpQuery = None,
|
||||
myipv6: RouterIpQuery = None,
|
||||
ip6addr: RouterIpQuery = None,
|
||||
) -> UpdateIpParams:
|
||||
return UpdateIpParams(myip=myip, ipaddr=ipaddr, myipv6=myipv6, ip6addr=ip6addr)
|
||||
|
||||
|
||||
RouterIpParamsDep = Annotated[UpdateIpParams, Depends(_ip_params_from_query)]
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -27,14 +43,15 @@ router = APIRouter()
|
||||
async def register_hostname_get(
|
||||
dashboard_secret: ManagementSlugPath,
|
||||
request: Request,
|
||||
store: StoreDep,
|
||||
service: ServiceDep,
|
||||
myip: str | None = Query(default=None),
|
||||
ipaddr: str | None = Query(default=None),
|
||||
myipv6: str | None = Query(default=None),
|
||||
ip6addr: str | None = Query(default=None),
|
||||
ip_params: RouterIpParamsDep,
|
||||
) -> str | PlainTextResponse:
|
||||
return await _register_hostname(dashboard_secret, request, store, service, myip, ipaddr, myipv6, ip6addr)
|
||||
return await _register_hostname(
|
||||
dashboard_secret,
|
||||
request,
|
||||
service,
|
||||
ip_params,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -47,45 +64,76 @@ async def register_hostname_get(
|
||||
async def register_hostname_post(
|
||||
dashboard_secret: ManagementSlugPath,
|
||||
request: Request,
|
||||
store: StoreDep,
|
||||
service: ServiceDep,
|
||||
myip: str | None = Query(default=None),
|
||||
ipaddr: str | None = Query(default=None),
|
||||
myipv6: str | None = Query(default=None),
|
||||
ip6addr: str | None = Query(default=None),
|
||||
ip_params: RouterIpParamsDep,
|
||||
) -> str | PlainTextResponse:
|
||||
return await _register_hostname(dashboard_secret, request, store, service, myip, ipaddr, myipv6, ip6addr)
|
||||
return await _register_hostname(
|
||||
dashboard_secret,
|
||||
request,
|
||||
service,
|
||||
ip_params,
|
||||
)
|
||||
|
||||
|
||||
async def _register_hostname(
|
||||
dashboard_secret: str,
|
||||
request: Request,
|
||||
store: StoreDep,
|
||||
service: ServiceDep,
|
||||
myip: str | None,
|
||||
ipaddr: str | None,
|
||||
myipv6: str | None,
|
||||
ip6addr: str | None,
|
||||
ip_params: _IpParams,
|
||||
) -> str | PlainTextResponse:
|
||||
myip, ipaddr, myipv6, ip6addr = await _ip_params_from_request(request, myip, ipaddr, myipv6, ip6addr)
|
||||
account = await run_in_threadpool(store.get_account_by_management_slug, dashboard_secret)
|
||||
if not account or account["disabled"]:
|
||||
ip_params = await _ip_params_from_request(request, ip_params)
|
||||
account = await run_in_threadpool(service.store.get_account_by_management_slug, dashboard_secret)
|
||||
hostname = _registration_hostname(account, service)
|
||||
assert account is not None
|
||||
dns_error = await _publish_initial_registration_dns(request, service, account, hostname, ip_params)
|
||||
if dns_error is not None:
|
||||
return dns_error
|
||||
activated = await run_in_threadpool(service.store.activate_account, hostname)
|
||||
if not activated:
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
await _log_registration_activation(request, service, hostname)
|
||||
return "good registered"
|
||||
|
||||
|
||||
def _registration_hostname(account: Mapping[str, object] | None, service: DdnsService) -> str:
|
||||
if not account or account.get("disabled"):
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
hostname = str(account["hostname"])
|
||||
if not service.hostname_is_managed(hostname):
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
if not account["accepted_at"]:
|
||||
try:
|
||||
await run_in_threadpool(service.publish_initial_dns, request, hostname, myip, ipaddr, myipv6, ip6addr)
|
||||
except BadIpError as exc:
|
||||
return PlainTextResponse(f"badip {exc}", status_code=400)
|
||||
except DnsProviderError:
|
||||
return PlainTextResponse("911 dns registration failed", status_code=500)
|
||||
activated = await run_in_threadpool(store.activate_account, hostname)
|
||||
if not activated:
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
return hostname
|
||||
|
||||
|
||||
async def _publish_initial_registration_dns(
|
||||
request: Request,
|
||||
service: DdnsService,
|
||||
account: Mapping[str, object],
|
||||
hostname: str,
|
||||
ip_params: _IpParams,
|
||||
) -> PlainTextResponse | None:
|
||||
if account.get("accepted_at"):
|
||||
return None
|
||||
try:
|
||||
await run_in_threadpool(
|
||||
service.publish_initial_dns,
|
||||
request,
|
||||
hostname,
|
||||
ip_params,
|
||||
)
|
||||
except BadIpError as exc:
|
||||
return PlainTextResponse(f"badip {exc}", status_code=400)
|
||||
except DnsProviderError:
|
||||
return PlainTextResponse("911 dns registration failed", status_code=500)
|
||||
return None
|
||||
|
||||
|
||||
async def _log_registration_activation(
|
||||
request: Request,
|
||||
service: DdnsService,
|
||||
hostname: str,
|
||||
) -> None:
|
||||
await run_in_threadpool(
|
||||
store.log_update_event,
|
||||
service.store.log_update_event,
|
||||
hostname,
|
||||
None,
|
||||
None,
|
||||
@@ -93,7 +141,6 @@ async def _register_hostname(
|
||||
"hostname registered",
|
||||
client_ip(request, service.settings),
|
||||
)
|
||||
return "good registered"
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -106,14 +153,15 @@ async def _register_hostname(
|
||||
async def update_hostname_get(
|
||||
update_slug: UpdateSlugPath,
|
||||
request: Request,
|
||||
store: StoreDep,
|
||||
service: ServiceDep,
|
||||
myip: str | None = Query(default=None),
|
||||
ipaddr: str | None = Query(default=None),
|
||||
myipv6: str | None = Query(default=None),
|
||||
ip6addr: str | None = Query(default=None),
|
||||
ip_params: RouterIpParamsDep,
|
||||
) -> str | PlainTextResponse:
|
||||
return await _update_hostname(update_slug, request, store, service, myip, ipaddr, myipv6, ip6addr)
|
||||
return await _update_hostname(
|
||||
update_slug,
|
||||
request,
|
||||
service,
|
||||
ip_params,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -126,76 +174,78 @@ async def update_hostname_get(
|
||||
async def update_hostname_post(
|
||||
update_slug: UpdateSlugPath,
|
||||
request: Request,
|
||||
store: StoreDep,
|
||||
service: ServiceDep,
|
||||
myip: str | None = Query(default=None),
|
||||
ipaddr: str | None = Query(default=None),
|
||||
myipv6: str | None = Query(default=None),
|
||||
ip6addr: str | None = Query(default=None),
|
||||
ip_params: RouterIpParamsDep,
|
||||
) -> str | PlainTextResponse:
|
||||
return await _update_hostname(update_slug, request, store, service, myip, ipaddr, myipv6, ip6addr)
|
||||
return await _update_hostname(
|
||||
update_slug,
|
||||
request,
|
||||
service,
|
||||
ip_params,
|
||||
)
|
||||
|
||||
|
||||
async def _update_hostname(
|
||||
update_slug: str,
|
||||
request: Request,
|
||||
store: StoreDep,
|
||||
service: ServiceDep,
|
||||
myip: str | None,
|
||||
ipaddr: str | None,
|
||||
myipv6: str | None,
|
||||
ip6addr: str | None,
|
||||
ip_params: _IpParams,
|
||||
) -> str | PlainTextResponse:
|
||||
myip, ipaddr, myipv6, ip6addr = await _ip_params_from_request(request, myip, ipaddr, myipv6, ip6addr)
|
||||
account = await run_in_threadpool(store.get_account_by_slug, update_slug)
|
||||
ip_params = await _ip_params_from_request(request, ip_params)
|
||||
account = await run_in_threadpool(service.store.get_account_by_slug, update_slug)
|
||||
if not account or account["disabled"] or not account["accepted_at"]:
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
if not service.hostname_is_managed(str(account["hostname"])):
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
return await run_in_threadpool(
|
||||
apply_update_response, service, request, str(account["hostname"]), myip, ipaddr, myipv6, ip6addr
|
||||
apply_update_response,
|
||||
service,
|
||||
request,
|
||||
str(account["hostname"]),
|
||||
ip_params,
|
||||
)
|
||||
|
||||
|
||||
async def _ip_params_from_request(
|
||||
request: Request,
|
||||
myip: str | None,
|
||||
ipaddr: str | None,
|
||||
myipv6: str | None,
|
||||
ip6addr: str | None,
|
||||
) -> tuple[str | None, str | None, str | None, str | None]:
|
||||
async def _ip_params_from_request(request: Request, ip_params: _IpParams) -> _IpParams:
|
||||
if request.method != "POST":
|
||||
return myip, ipaddr, myipv6, ip6addr
|
||||
return ip_params
|
||||
body_values = await _post_values(request)
|
||||
return (
|
||||
myip or body_values.get("myip"),
|
||||
ipaddr or body_values.get("ipaddr"),
|
||||
myipv6 or body_values.get("myipv6"),
|
||||
ip6addr or body_values.get("ip6addr"),
|
||||
)
|
||||
return ip_params.with_body_values(body_values)
|
||||
|
||||
|
||||
async def _post_values(request: Request) -> dict[str, str]:
|
||||
content_type = request.headers.get("content-type", "").split(";", 1)[0].strip().lower()
|
||||
body = await request.body()
|
||||
if not body:
|
||||
return {}
|
||||
content_type = _request_content_type(request)
|
||||
if content_type == "application/json":
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return {str(key): str(value) for key, value in data.items() if isinstance(value, str)}
|
||||
return _json_post_values(body)
|
||||
if content_type == "application/x-www-form-urlencoded":
|
||||
try:
|
||||
form_body = body.decode("utf-8", errors="strict")
|
||||
except UnicodeDecodeError:
|
||||
return {}
|
||||
try:
|
||||
parsed = parse_qs(form_body, keep_blank_values=True, max_num_fields=8)
|
||||
except ValueError:
|
||||
return {}
|
||||
return {key: values[0] for key, values in parsed.items() if values}
|
||||
return _form_post_values(body)
|
||||
return {}
|
||||
|
||||
|
||||
def _request_content_type(request: Request) -> str:
|
||||
return request.headers.get("content-type", "").split(";", 1)[0].strip().lower()
|
||||
|
||||
|
||||
def _json_post_values(body: bytes) -> dict[str, str]:
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return {str(key): value for key, value in data.items() if isinstance(value, str)}
|
||||
|
||||
|
||||
def _form_post_values(body: bytes) -> dict[str, str]:
|
||||
try:
|
||||
form_body = body.decode("utf-8", errors="strict")
|
||||
except UnicodeDecodeError:
|
||||
return {}
|
||||
try:
|
||||
parsed = parse_qs(form_body, keep_blank_values=True, max_num_fields=8)
|
||||
except ValueError:
|
||||
return {}
|
||||
return {key: values[0] for key, values in parsed.items() if values}
|
||||
Reference in new issue
Block a user