""" webui/api/certs.py - ACME certificate management API blueprint. Exposed at /api/certs/* and delegates to lib.acme. """ import logging from flask import Blueprint, jsonify, request from lib.acme import ( get_cert_info, issue, list_certs, remove, renew, set_email, ) logger = logging.getLogger(__name__) bp = Blueprint("certs", __name__) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _error(msg, code=400): return jsonify({"ok": False, "error": msg}), code def _ok(data=None): return jsonify({"ok": True, "data": data}) # --------------------------------------------------------------------------- # Certificate listing # --------------------------------------------------------------------------- @bp.route("/list", methods=["GET"]) def list_certs_bp(): try: return _ok(list_certs()) 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): try: info = get_cert_info(domain) return _ok(info) except ValueError as 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) # --------------------------------------------------------------------------- # 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") try: logger.info("Certificate issuance requested for '%s' via API", domain) result = issue(domain, webroot=webroot) if result.get("success"): logger.info("Certificate issued for '%s'", domain) return _ok(None) logger.error( "Certificate issuance failed for '%s': %s", domain, result.get("error") ) return _error(result.get("error", "Unknown error"), 500) except RuntimeError as exc: logger.error("Exception issuing cert for '%s': %s", domain, exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Renew # --------------------------------------------------------------------------- @bp.route("//renew", methods=["POST"]) def renew_bp(domain): try: logger.info("Certificate renewal requested for '%s' via API", domain) result = renew(domain) if result.get("success"): logger.info("Certificate renewed for '%s'", domain) return _ok(None) logger.error( "Certificate renewal failed for '%s': %s", domain, result.get("error") ) return _error(result.get("error", "Unknown error"), 500) except RuntimeError as exc: logger.error("Exception renewing cert for '%s': %s", domain, exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Remove # --------------------------------------------------------------------------- @bp.route("/", methods=["DELETE"]) def remove_bp(domain): try: get_cert_info(domain) except ValueError as exc: return _error(str(exc), 404) except RuntimeError 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 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 as exc: logger.error("Failed to set ACME email: %s", exc) return _error(str(exc), 500)