"""ACME certificate management API blueprint. Exposed at /api/certs/* and delegates to vacuum-walld. """ import logging from flask import Blueprint, request from daemon.client import BadRequest, NotFound, delete, get, post from webui.api.common import _error, _ok logger = logging.getLogger(__name__) bp = Blueprint("certs", __name__) @bp.route("/list", methods=["GET"]) def list_certs_bp(): try: return _ok(get("/acme/list")) except RuntimeError as exc: logger.error("Failed to list certificates: %s", exc) return _error(str(exc), 500) @bp.route("/", methods=["GET"]) def cert_details(domain: str): try: return _ok(get("/acme/info", {"domain": domain})) except NotFound as exc: logger.info("Cert for '%s' not found: %s", domain, exc) return _error(str(exc), 404) except RuntimeError as exc: logger.error("Failed to get cert info for '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("/issue", methods=["POST"]) def issue_bp(): body = request.get_json(silent=True) or {} domain = body.get("domain", "").strip() if not domain: return _error("'domain' is required", 400) webroot = body.get("webroot") email = body.get("email", "").strip() or None try: logger.info("Certificate issuance requested for '%s' via API", domain) post("/acme/issue", {"domain": domain, "webroot": webroot, "email": email}) logger.info("Certificate issued for '%s'", domain) return _ok(None) except BadRequest as exc: logger.info("Cert issue for '%s' rejected: %s", domain, exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to issue cert for '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("//renew", methods=["POST"]) def renew_bp(domain: str): try: logger.info("Certificate renewal requested for '%s' via API", domain) post("/acme/renew", {"domain": domain}) logger.info("Certificate renewed for '%s'", domain) return _ok(None) except BadRequest as exc: logger.info("Cert renew for '%s' rejected: %s", domain, exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to renew cert for '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("/", methods=["DELETE"]) def remove_bp(domain: str): try: delete("/acme/remove", {"domain": domain}) logger.info("Certificate removed for '%s' via API", domain) return _ok(None) except NotFound as exc: logger.info("Cert '%s' not found: %s", domain, exc) return _error(str(exc), 404) except RuntimeError as exc: logger.error("Failed to remove cert '%s': %s", domain, exc) return _error(str(exc), 500) @bp.route("/email", methods=["POST"]) def set_email_bp(): body = request.get_json(silent=True) or {} email = body.get("email", "").strip() if not email: return _error("'email' is required", 400) try: post("/acme/email", {"email": email}) logger.info("ACME email set via API: %s", email) return _ok({"email": email}) except BadRequest as exc: logger.info("ACME email set rejected: %s", exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to set ACME email: %s", exc) return _error(str(exc), 500)