""" webui/api/proxy.py - Nginx proxy domain management API blueprint. Exposed at /api/proxy/* and delegates to lib.nginx. """ import logging from flask import Blueprint, request from lib.common import deep_merge from lib.nginx import ( add_domain, apply, get_config, get_domains, remove_domain, save_config, set_management_proxy, test_config, update_domain, write_ssl_snippet, ) 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 (write) the global SSL snippet for all Nginx server blocks.""" try: write_ssl_snippet() 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) # --------------------------------------------------------------------------- # Config (declarative) # --------------------------------------------------------------------------- @bp.route("/config", methods=["GET"]) def get_config_bp(): try: return _ok(get_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(): body = request.get_json(silent=True) or {} if not isinstance(body, dict): return _error("Request body must be a JSON object", 400) try: save_config(body) logger.info("Proxy config saved: %s", sorted(body.keys())) return _ok(None) 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(): body = request.get_json(silent=True) or {} if not isinstance(body, dict): return _error("Request body must be a JSON object", 400) try: current = get_config() merged = deep_merge(current, body) save_config(merged) logger.info("Proxy config patched: %s", sorted(body.keys())) return _ok(None) except RuntimeError as exc: logger.error("Failed to patch proxy config: %s", exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Domains # --------------------------------------------------------------------------- @bp.route("/domains", methods=["GET"]) def list_domains(): try: return _ok(get_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(): 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: add_domain( domain, backend_host, int(backend_port), backend_proto, cert, extra_headers ) logger.info("Proxy domain added via API: %s", domain) return _ok({"domain": domain}) except (ValueError, RuntimeError) as exc: logger.error("Failed to add proxy domain '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("/domains/", methods=["GET"]) def domain_details(domain): try: cfg = get_config() entry = cfg.get("domains", {}).get(domain) if entry is None: return _error(f"Domain '{domain}' not found", 404) return _ok({"domain": domain, **entry}) except RuntimeError as exc: logger.error("Failed to get domain details: %s", exc) return _error(str(exc), 500) @bp.route("/domains/", methods=["PUT"]) def update_domain_bp(domain): 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: update_domain(domain, **body) logger.info("Proxy domain '%s' updated via API", domain) return _ok({"domain": domain}) except KeyError as 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): try: cfg = get_config() if domain not in cfg.get("domains", {}): return _error(f"Domain '{domain}' not found", 404) remove_domain(domain) logger.info("Proxy domain removed via API: %s", domain) return _ok({"domain": domain}) except RuntimeError as exc: logger.error("Failed to remove domain '%s': %s", domain, exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Apply / test # --------------------------------------------------------------------------- @bp.route("/apply", methods=["POST"]) def apply_bp(): try: 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(): try: valid, output = test_config() if valid: return _ok({"valid": True, "output": output}) return _error(output, 400) except RuntimeError as exc: logger.error("nginx config test failed: %s", exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Management proxy # --------------------------------------------------------------------------- @bp.route("/management", methods=["POST"]) def management_bp(): 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: set_management_proxy(domain, flask_host, int(flask_port), auth_user, auth_pass) logger.info("Management proxy configured via API: %s", domain) return _ok(None) except (ValueError, RuntimeError) as exc: code = 400 if isinstance(exc, ValueError) else 500 logger.error("Failed to set management proxy: %s", exc) return _error(str(exc), code)