"""Nginx proxy domain management API blueprint. Exposed at /api/proxy/* and delegates to vacuum-walld. """ import logging from flask import Blueprint, request from daemon.client import BadRequest, NotFound, delete, get, patch, post from webui.api.common import _error, _ok logger = logging.getLogger(__name__) bp = Blueprint("proxy", __name__) @bp.route("/ssl-apply", methods=["POST"]) def ssl_apply_bp(): """Apply SSL snippet config. POST /api/proxy/ssl-apply Returns: ``{"ok": true}`` on success. Raises: RuntimeError: If nginx SSL snippet write fails. """ try: post("/nginx/ssl-apply") logger.info("SSL snippet written via API") return _ok(None) except RuntimeError as exc: logger.error("Failed to write SSL snippet: %s", exc) return _error(str(exc), 500) @bp.route("/config", methods=["GET"]) def get_config_bp(): """Get the current nginx proxy configuration. GET /api/proxy/config Returns: Current config dict from the daemon. """ try: return _ok(get("/nginx/config")) except RuntimeError as exc: logger.error("Failed to read proxy config: %s", exc) return _error(str(exc), 500) @bp.route("/config", methods=["POST"]) def post_config(): """Save the nginx proxy configuration. POST /api/proxy/config Body: Any JSON object to merge into the config. Returns: ``{"ok": true}`` on success. """ body = request.get_json(silent=True) or {} if not isinstance(body, dict): return _error("Request body must be a JSON object", 400) try: post("/nginx/config", body) logger.info("Proxy config saved: %s", sorted(body.keys())) return _ok(None) except BadRequest as exc: logger.info("Proxy config save rejected: %s", exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to save proxy config: %s", exc) return _error(str(exc), 500) @bp.route("/config", methods=["PATCH"]) def patch_config(): """Partially update the nginx proxy configuration. PATCH /api/proxy/config Body: JSON object with fields to patch. Returns: ``{"ok": true}`` on success. """ body = request.get_json(silent=True) or {} if not isinstance(body, dict): return _error("Request body must be a JSON object", 400) try: patch("/nginx/config", body) logger.info("Proxy config patched: %s", sorted(body.keys())) return _ok(None) except BadRequest as exc: logger.info("Proxy config patch rejected: %s", exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to patch proxy config: %s", exc) return _error(str(exc), 500) @bp.route("/domains", methods=["GET"]) def list_domains(): """List all configured proxy domains. GET /api/proxy/domains Returns: List of domain dicts from the daemon. """ try: return _ok(get("/nginx/domains")) except RuntimeError as exc: logger.error("Failed to list proxy domains: %s", exc) return _error(str(exc), 500) @bp.route("/domains", methods=["POST"]) def add_domain_bp(): """Add a new proxy domain. POST /api/proxy/domains Body fields: domain: Domain name. backend_host: Upstream host. backend_port: Upstream port. backend_proto: Protocol (``http`` or ``https``, default ``http``). cert: Optional certificate type. extra_headers: Optional extra headers dict. Returns: ``{"domain": ...}`` on success. """ body = request.get_json(silent=True) or {} domain = body.get("domain", "").strip() backend_host = body.get("backend_host", "").strip() backend_port = body.get("backend_port") backend_proto = body.get("backend_proto", "http").strip() or "http" cert = body.get("cert") extra_headers = body.get("extra_headers") if not domain: return _error("'domain' is required", 400) if not backend_host: return _error("'backend_host' is required", 400) if backend_port is None: return _error("'backend_port' is required", 400) try: post( "/nginx/domains/add", { "domain": domain, "backend_host": backend_host, "backend_port": int(backend_port), "backend_proto": backend_proto, "cert": cert, "extra_headers": extra_headers, }, ) logger.info("Proxy domain added via API: %s", domain) return _ok({"domain": domain}) except BadRequest as exc: logger.info("Add proxy domain '%s' rejected: %s", domain, exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to add proxy domain '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("/domains/", methods=["PUT"]) def update_domain_bp(domain): """Update an existing proxy domain in-place. PUT /api/proxy/domains/ Body fields: Fields to merge into the domain config. Returns: ``{"domain": ...}`` on success. """ body = request.get_json(silent=True) or {} if not body: return _error("Request body must be a JSON object with fields to update", 400) try: post("/nginx/domains/update", {"domain": domain, **body}) logger.info("Proxy domain '%s' updated via API", domain) return _ok({"domain": domain}) except BadRequest as exc: logger.info("Update domain '%s' rejected: %s", domain, exc) return _error(str(exc), 400) except NotFound as exc: logger.info("Domain '%s' not found: %s", domain, exc) return _error(str(exc), 404) except RuntimeError as exc: logger.error("Failed to update domain '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("/domains/", methods=["DELETE"]) def remove_domain_bp(domain): """Remove a proxy domain. DELETE /api/proxy/domains/ Returns: ``{"domain": ...}`` on success. """ try: delete("/nginx/domains/remove", {"domain": domain}) logger.info("Proxy domain removed via API: %s", domain) return _ok({"domain": domain}) except NotFound as exc: logger.info("Domain '%s' not found: %s", domain, exc) return _error(str(exc), 404) except RuntimeError as exc: logger.error("Failed to remove domain '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("/apply", methods=["POST"]) def apply_bp(): """Generate all nginx configs and reload nginx. POST /api/proxy/apply Returns: ``{"ok": true}`` on success. """ try: post("/nginx/apply") logger.info("nginx config applied via API") return _ok(None) except RuntimeError as exc: logger.error("Failed to apply nginx config: %s", exc) return _error(str(exc), 500) @bp.route("/test", methods=["POST"]) def test_bp(): """Test nginx configuration without reloading. POST /api/proxy/test Returns: ``{"valid": true, "output": ...}`` on success. Returns 400 if test fails. """ try: result = post("/nginx/test") if result.get("valid"): return _ok({"valid": True, "output": result.get("output", "")}) return _error(result.get("output", "unknown error"), 400) except RuntimeError as exc: logger.error("nginx config test failed: %s", exc) return _error(str(exc), 500) @bp.route("/management", methods=["POST"]) def management_bp(): """Configure the management reverse proxy for the WebUI. POST /api/proxy/management Body fields: domain: Management domain name. flask_host: Upstream Flask host (default ``127.0.0.1``). flask_port: Upstream Flask port (default 9090). auth_user: Optional basic-auth username. auth_pass: Optional basic-auth password. Returns: ``{"ok": true}`` on success. """ body = request.get_json(silent=True) or {} domain = body.get("domain", "").strip() if not domain: return _error("'domain' is required", 400) flask_host = body.get("flask_host", "127.0.0.1").strip() or "127.0.0.1" flask_port = body.get("flask_port", 9090) auth_user = body.get("auth_user") auth_pass = body.get("auth_pass") try: post( "/nginx/management", { "domain": domain, "flask_host": flask_host, "flask_port": int(flask_port), "auth_user": auth_user, "auth_pass": auth_pass, }, ) logger.info("Management proxy configured via API: %s", domain) return _ok(None) except BadRequest as exc: logger.info("Management proxy config rejected: %s", exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to set management proxy: %s", exc) return _error(str(exc), 500)