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
+70
-44
@@ -6,6 +6,34 @@ from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
_RESERVED_PUBLIC_PATHS = {
|
||||
"/admin.css",
|
||||
"/admin-tables.js",
|
||||
"/api",
|
||||
"/app.js",
|
||||
"/clipboard.js",
|
||||
"/docs",
|
||||
"/favicon.ico",
|
||||
"/healthz",
|
||||
"/install.sh",
|
||||
"/bootstrap-subset.css",
|
||||
"/logo.png",
|
||||
"/logo-nav.webp",
|
||||
"/logo.webp",
|
||||
"/m",
|
||||
"/openapi.json",
|
||||
"/privacy",
|
||||
"/redoc",
|
||||
"/robots.txt",
|
||||
"/sitemap.xml",
|
||||
"/site.webmanifest",
|
||||
"/social-preview.png",
|
||||
"/social-preview.webp",
|
||||
"/theme.js",
|
||||
"/uninstall.sh",
|
||||
"/visitor-ip.js",
|
||||
}
|
||||
|
||||
|
||||
class DdnsSettings(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -42,52 +70,25 @@ class DdnsSettings(BaseModel):
|
||||
|
||||
@field_validator("public_base_url")
|
||||
@classmethod
|
||||
def validate_public_base_url(cls, value: str) -> str:
|
||||
def validate_public_base_url(_cls, value: str) -> str:
|
||||
if not value.startswith(("http://", "https://")):
|
||||
raise ValueError("public_base_url must start with http:// or https://")
|
||||
return value.rstrip("/")
|
||||
|
||||
@field_validator("trusted_hosts")
|
||||
@classmethod
|
||||
def validate_trusted_hosts(cls, value: set[str]) -> set[str]:
|
||||
def validate_trusted_hosts(_cls, value: set[str]) -> set[str]:
|
||||
return {host.strip().lower() for host in value if host.strip()}
|
||||
|
||||
@field_validator("admin_path")
|
||||
@classmethod
|
||||
def validate_admin_path(cls, value: str) -> str:
|
||||
def validate_admin_path(_cls, value: str) -> str:
|
||||
path = value.strip()
|
||||
if not path.startswith("/") or path == "/" or path.endswith("/"):
|
||||
if not _has_valid_admin_path_shape(path):
|
||||
raise ValueError("admin_path must start with / and must not be / or end with /")
|
||||
if not re.fullmatch(r"/[A-Za-z0-9._~/-]+", path):
|
||||
if not _has_url_safe_path_chars(path):
|
||||
raise ValueError("admin_path may only contain URL-safe path characters")
|
||||
reserved_paths = {
|
||||
"/admin.css",
|
||||
"/admin-tables.js",
|
||||
"/api",
|
||||
"/app.js",
|
||||
"/clipboard.js",
|
||||
"/docs",
|
||||
"/favicon.ico",
|
||||
"/healthz",
|
||||
"/install.sh",
|
||||
"/bootstrap-subset.css",
|
||||
"/logo.png",
|
||||
"/logo-nav.webp",
|
||||
"/logo.webp",
|
||||
"/m",
|
||||
"/openapi.json",
|
||||
"/privacy",
|
||||
"/redoc",
|
||||
"/robots.txt",
|
||||
"/sitemap.xml",
|
||||
"/site.webmanifest",
|
||||
"/social-preview.png",
|
||||
"/social-preview.webp",
|
||||
"/theme.js",
|
||||
"/uninstall.sh",
|
||||
"/visitor-ip.js",
|
||||
}
|
||||
if any(path == reserved or path.startswith(f"{reserved}/") for reserved in reserved_paths):
|
||||
if _is_reserved_admin_path(path):
|
||||
raise ValueError("admin_path must not conflict with a public route")
|
||||
return path
|
||||
|
||||
@@ -107,19 +108,44 @@ class DdnsSettings(BaseModel):
|
||||
validate_dns_provider_settings(self)
|
||||
if not self.require_dns_provider:
|
||||
return
|
||||
if len(self.admin_password) < 16:
|
||||
raise RuntimeError("DDNS_ADMIN_PASSWORD must be at least 16 characters when DDNS_REQUIRE_DNS_PROVIDER=1")
|
||||
if len(self.dashboard_cookie_secret or self.admin_password) < 32:
|
||||
raise RuntimeError(
|
||||
"DDNS_DASHBOARD_COOKIE_SECRET or DDNS_ADMIN_PASSWORD must be at least 32 characters for dashboard cookie signing when DDNS_REQUIRE_DNS_PROVIDER=1"
|
||||
)
|
||||
if not self.hostname_suffix:
|
||||
raise RuntimeError("DDNS_HOSTNAME_SUFFIX is required when DDNS_REQUIRE_DNS_PROVIDER=1")
|
||||
if not self.effective_trusted_hosts:
|
||||
raise RuntimeError("At least one trusted host is required for production")
|
||||
_validate_launch_secrets(self.admin_password, self.dashboard_cookie_secret)
|
||||
_validate_launch_hostname_suffix(self.hostname_suffix)
|
||||
_validate_launch_trusted_hosts(self.effective_trusted_hosts)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> DdnsSettings:
|
||||
def from_env(_cls) -> DdnsSettings:
|
||||
from .config import settings_from_env
|
||||
|
||||
return settings_from_env()
|
||||
|
||||
|
||||
def _has_valid_admin_path_shape(path: str) -> bool:
|
||||
return path.startswith("/") and path != "/" and not path.endswith("/")
|
||||
|
||||
|
||||
def _has_url_safe_path_chars(path: str) -> bool:
|
||||
return re.fullmatch(r"/[A-Za-z0-9._~/-]+", path) is not None
|
||||
|
||||
|
||||
def _is_reserved_admin_path(path: str) -> bool:
|
||||
return any(path == reserved or path.startswith(f"{reserved}/") for reserved in _RESERVED_PUBLIC_PATHS)
|
||||
|
||||
|
||||
def _validate_launch_secrets(admin_password: str, dashboard_cookie_secret: str) -> None:
|
||||
if len(admin_password) < 16:
|
||||
raise RuntimeError("DDNS_ADMIN_PASSWORD must be at least 16 characters when DDNS_REQUIRE_DNS_PROVIDER=1")
|
||||
if len(dashboard_cookie_secret or admin_password) < 32:
|
||||
raise RuntimeError(
|
||||
"DDNS_DASHBOARD_COOKIE_SECRET or DDNS_ADMIN_PASSWORD must be at least 32 characters "
|
||||
"for dashboard cookie signing when DDNS_REQUIRE_DNS_PROVIDER=1"
|
||||
)
|
||||
|
||||
|
||||
def _validate_launch_hostname_suffix(hostname_suffix: str) -> None:
|
||||
if not hostname_suffix:
|
||||
raise RuntimeError("DDNS_HOSTNAME_SUFFIX is required when DDNS_REQUIRE_DNS_PROVIDER=1")
|
||||
|
||||
|
||||
def _validate_launch_trusted_hosts(effective_trusted_hosts: list[str]) -> None:
|
||||
if not effective_trusted_hosts:
|
||||
raise RuntimeError("At least one trusted host is required for production")
|
||||
Reference in new issue
Block a user