"""ACME certificate management API blueprint. Exposed at /api/certs/* and delegates to lib.acme. """ import logging from flask import Blueprint, request from lib.acme import ( get_cert_info, issue, list_certs, remove, renew, set_email, ) from webui.api.common import _error, _ok logger = logging.getLogger(__name__) bp = Blueprint("certs", __name__) # --------------------------------------------------------------------------- # Certificate listing # --------------------------------------------------------------------------- @bp.route("/list", methods=["GET"]) def list_certs_bp(): try: return _ok(list_certs()) except (RuntimeError, FileNotFoundError) 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: info = get_cert_info(domain) return _ok(info) except ValueError as exc: return _error(str(exc), 404) except (RuntimeError, FileNotFoundError) as exc: logger.error("Failed to get cert info for '%s': %s", domain, exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Issue # --------------------------------------------------------------------------- @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) issue(domain, webroot=webroot, email=email) logger.info("Certificate issued for '%s'", domain) return _ok(None) except (RuntimeError, FileNotFoundError) as exc: logger.error("Failed to issue cert for '%s': %s", domain, exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Renew # --------------------------------------------------------------------------- @bp.route("//renew", methods=["POST"]) def renew_bp(domain: str): try: logger.info("Certificate renewal requested for '%s' via API", domain) renew(domain) logger.info("Certificate renewed for '%s'", domain) return _ok(None) except (RuntimeError, FileNotFoundError) as exc: logger.error("Failed to renew cert for '%s': %s", domain, exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Remove # --------------------------------------------------------------------------- @bp.route("/", methods=["DELETE"]) def remove_bp(domain: str): try: get_cert_info(domain) except ValueError as exc: return _error(str(exc), 404) except (RuntimeError, FileNotFoundError) as exc: logger.error("Failed to verify cert '%s': %s", domain, exc) return _error(str(exc), 500) try: remove(domain) logger.info("Certificate removed for '%s' via API", domain) return _ok(None) except (RuntimeError, FileNotFoundError) as exc: logger.error("Failed to remove cert '%s': %s", domain, exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Contact email # --------------------------------------------------------------------------- @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: set_email(email) logger.info("ACME email set via API: %s", email) return _ok({"email": email}) except (RuntimeError, FileNotFoundError) as exc: logger.error("Failed to set ACME email: %s", exc) return _error(str(exc), 500)