feat: strengthen module contracts and shared WebUI runtime
This commit is contained in:
@@ -72,8 +72,19 @@ def _validate_production_startup() -> None:
|
||||
"off",
|
||||
}
|
||||
if throttle_enabled and not os.getenv("REDIS_URL", "").strip():
|
||||
allow_process_local = os.getenv(
|
||||
"GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE",
|
||||
"false",
|
||||
).strip().lower() in {"1", "true", "yes", "on"}
|
||||
if not allow_process_local:
|
||||
raise RuntimeError(
|
||||
"Production login throttling requires REDIS_URL. "
|
||||
"Set GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE=true only "
|
||||
"for an explicitly single-process deployment."
|
||||
)
|
||||
logger.warning(
|
||||
"Redis is not configured; login throttling is process-local and will not coordinate horizontally"
|
||||
"Redis is not configured; login throttling is explicitly limited "
|
||||
"to this process"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from govoplan_core.admin.models import SystemSettings
|
||||
from govoplan_core.admin.settings import SYSTEM_SETTINGS_ID
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal
|
||||
from govoplan_core.core.maintenance import saved_maintenance_mode
|
||||
from govoplan_core.core.modules import FrontendModule, FrontendRoute, ModuleManifest, NavItem, PublicFrontendRoute
|
||||
from govoplan_core.core.registry import PlatformRegistry, manifest_view_surfaces
|
||||
@@ -153,7 +154,10 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
|
||||
}
|
||||
|
||||
@router.get("/modules")
|
||||
def modules(request: Request):
|
||||
def modules(
|
||||
request: Request,
|
||||
_principal: ApiPrincipal = Depends(get_api_principal),
|
||||
):
|
||||
registry = _registry(request)
|
||||
return {
|
||||
"modules": [
|
||||
@@ -190,7 +194,10 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
|
||||
}
|
||||
|
||||
@router.get("/permissions")
|
||||
def permissions(request: Request):
|
||||
def permissions(
|
||||
request: Request,
|
||||
_principal: ApiPrincipal = Depends(get_api_principal),
|
||||
):
|
||||
registry = _registry(request)
|
||||
return {
|
||||
"permissions": [
|
||||
@@ -210,7 +217,10 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
|
||||
}
|
||||
|
||||
@router.get("/navigation")
|
||||
def navigation(request: Request):
|
||||
def navigation(
|
||||
request: Request,
|
||||
_principal: ApiPrincipal = Depends(get_api_principal),
|
||||
):
|
||||
registry = _registry(request)
|
||||
return {
|
||||
"items": [_nav_item_payload(item) for item in registry.nav_items()]
|
||||
|
||||
@@ -3,6 +3,9 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
import importlib
|
||||
|
||||
from govoplan_core.core.access import (
|
||||
DEFAULT_CAPABILITY_PROVIDERS,
|
||||
)
|
||||
from govoplan_core.core.discovery import discover_module_manifests
|
||||
from govoplan_core.core.modules import ModuleManifest
|
||||
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
||||
@@ -15,7 +18,6 @@ _BUILTIN_MANIFESTS = {
|
||||
"tenancy": "govoplan_tenancy.backend.manifest:get_manifest",
|
||||
}
|
||||
|
||||
|
||||
def parse_enabled_modules(value: str | Iterable[str]) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
items = [item.strip() for item in value.split(",")]
|
||||
@@ -57,22 +59,68 @@ def available_module_manifests(
|
||||
return manifests
|
||||
|
||||
|
||||
def build_platform_registry(enabled_modules: str | Iterable[str], *, manifest_factories: Sequence[ManifestFactory] = ()) -> PlatformRegistry:
|
||||
def build_platform_registry(
|
||||
enabled_modules: str | Iterable[str],
|
||||
*,
|
||||
manifest_factories: Sequence[ManifestFactory] = (),
|
||||
) -> PlatformRegistry:
|
||||
requested = parse_enabled_modules(enabled_modules)
|
||||
if "access" not in requested:
|
||||
requested.insert(0, "access")
|
||||
|
||||
available = available_module_manifests(manifest_factories, enabled_modules=requested)
|
||||
available = _resolve_required_manifests(
|
||||
requested,
|
||||
manifest_factories=manifest_factories,
|
||||
)
|
||||
registry = PlatformRegistry()
|
||||
for module_id in requested:
|
||||
manifest = available.get(module_id)
|
||||
if manifest is None:
|
||||
raise RegistryError(f"Enabled module is not available: {module_id}")
|
||||
for module_id, manifest in available.items():
|
||||
registry.register(manifest)
|
||||
registry.validate()
|
||||
return registry
|
||||
|
||||
|
||||
def _resolve_required_manifests(
|
||||
requested: Sequence[str],
|
||||
*,
|
||||
manifest_factories: Sequence[ManifestFactory],
|
||||
) -> dict[str, ModuleManifest]:
|
||||
manifests: dict[str, ModuleManifest] = {}
|
||||
pending = list(dict.fromkeys(requested))
|
||||
|
||||
while pending:
|
||||
module_ids = [module_id for module_id in pending if module_id not in manifests]
|
||||
pending = []
|
||||
if not module_ids:
|
||||
break
|
||||
|
||||
available = available_module_manifests(
|
||||
manifest_factories,
|
||||
enabled_modules=module_ids,
|
||||
)
|
||||
for module_id in module_ids:
|
||||
manifest = available.get(module_id)
|
||||
if manifest is None:
|
||||
raise RegistryError(f"Enabled module is not available: {module_id}")
|
||||
manifests[module_id] = manifest
|
||||
|
||||
provided_capabilities = {
|
||||
capability
|
||||
for manifest in manifests.values()
|
||||
for capability in manifest.capability_factories
|
||||
}
|
||||
for manifest in tuple(manifests.values()):
|
||||
pending.extend(
|
||||
dependency_id
|
||||
for dependency_id in manifest.dependencies
|
||||
if dependency_id not in manifests
|
||||
)
|
||||
for capability in manifest.required_capabilities:
|
||||
if capability in provided_capabilities:
|
||||
continue
|
||||
provider_id = DEFAULT_CAPABILITY_PROVIDERS.get(capability)
|
||||
if provider_id is not None and provider_id not in manifests:
|
||||
pending.append(provider_id)
|
||||
|
||||
return manifests
|
||||
|
||||
|
||||
def _load_builtin_manifest(module_id: str) -> ModuleManifest:
|
||||
target = _BUILTIN_MANIFESTS[module_id]
|
||||
module_name, function_name = validate_object_path(target)
|
||||
|
||||
Reference in New Issue
Block a user