feat: add ACME account management with validation pipeline
- Register, view, and deactivate ACME accounts via API and UI - 16-check validation framework for certificate issuance readiness - DNS resolution, port, nginx, and firewall pre-flight checks - External IP detection with NAT support and fallback providers - Account card and settings modal in certificates page - Guard certificate issuance behind account registration - Update modal CSS to overlay-based approach - 1000+ lines of tests for validation and account handlers
This commit is contained in:
@@ -586,6 +586,68 @@ def _parse_acme_list_output(raw: str) -> list[dict]:
|
||||
return entries
|
||||
|
||||
|
||||
def _parse_account_conf(acme_home: Path | None = None) -> dict[str, Any]:
|
||||
"""Parse acme.sh .account.conf and return account status dict.
|
||||
|
||||
Args:
|
||||
acme_home: Optional override for ACME home directory. Falls back
|
||||
to ``ACME_HOME`` env var or ``PROJECT_DIR/data/acme``.
|
||||
|
||||
Returns:
|
||||
Dict with ``registered``, ``email``, ``ca``, and
|
||||
``key_length`` keys. If the file is missing or keys are absent,
|
||||
``registered`` is ``False`` with empty / ``None`` values.
|
||||
"""
|
||||
if acme_home is None:
|
||||
acme_home_env = os.environ.get("ACME_HOME", str(PROJECT_DIR / "data" / "acme"))
|
||||
acme_home = Path(acme_home_env)
|
||||
account_path = acme_home / ".account.conf"
|
||||
|
||||
default = {
|
||||
"registered": False,
|
||||
"email": "",
|
||||
"ca": "",
|
||||
"key_length": None,
|
||||
}
|
||||
|
||||
if not account_path.is_file():
|
||||
return default
|
||||
|
||||
try:
|
||||
text = account_path.read_text()
|
||||
except OSError:
|
||||
return default
|
||||
|
||||
email = ""
|
||||
ca_raw = ""
|
||||
key_length = None
|
||||
|
||||
for line in text.splitlines():
|
||||
if line.startswith("ACME_LEEMAIL="):
|
||||
email = line.split("=", 1)[1].strip().strip("'\"")
|
||||
elif line.startswith("ACME_MCA="):
|
||||
ca_raw = line.split("=", 1)[1].strip().strip("'\"")
|
||||
elif line.startswith("ACME_CERTKEYSIZE="):
|
||||
raw_val = line.split("=", 1)[1].strip().strip("'\"")
|
||||
key_length = int(raw_val) if raw_val.isdigit() else None
|
||||
|
||||
if not email or not ca_raw:
|
||||
return default
|
||||
|
||||
ca_map = {
|
||||
"letsencrypt": "Let's Encrypt",
|
||||
"zerossl": "ZeroSSL",
|
||||
}
|
||||
ca = ca_map.get(ca_raw, ca_raw)
|
||||
|
||||
return {
|
||||
"registered": True,
|
||||
"email": email,
|
||||
"ca": ca,
|
||||
"key_length": key_length,
|
||||
}
|
||||
|
||||
|
||||
def _has_auto_renew(domain: str) -> bool:
|
||||
"""Check whether *domain* has an auto-renew configuration file.
|
||||
|
||||
@@ -653,9 +715,12 @@ def _collect_acme() -> dict[str, Any]:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
account = _parse_account_conf()
|
||||
|
||||
return {
|
||||
"certs": certs,
|
||||
"email": email,
|
||||
"account": account,
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user