From 9bc48a3e947e45e0c1e5f19932dd61fdd1e0183d Mon Sep 17 00:00:00 2001 From: twothatit Date: Sun, 8 Feb 2026 22:31:49 +0100 Subject: [PATCH] Fix NPM: reuse existing proxy host and SSL cert on redeployment When a proxy host already exists in NPM (domain "already in use"), the code now finds the existing host, updates it, and requests SSL instead of failing with an error. Also checks if the host already has a valid certificate before requesting a new one from Let's Encrypt. Co-Authored-By: Claude Opus 4.6 --- app/services/npm_service.py | 76 ++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/app/services/npm_service.py b/app/services/npm_service.py index c18b6f0..73a5727 100644 --- a/app/services/npm_service.py +++ b/app/services/npm_service.py @@ -112,6 +112,29 @@ async def test_npm_connection(api_url: str, email: str, password: str) -> dict[s return {"ok": False, "message": f"Unexpected error: {exc}"} +async def _find_proxy_host_by_domain( + client: httpx.AsyncClient, api_url: str, headers: dict, domain: str +) -> int | None: + """Find an existing proxy host by domain name. + + Args: + client: httpx client (already authenticated). + api_url: NPM API base URL. + headers: Auth headers with Bearer token. + domain: Domain to search for. + + Returns: + Proxy host ID if found, None otherwise. + """ + resp = await client.get(f"{api_url}/nginx/proxy-hosts", headers=headers) + if resp.status_code == 200: + for host in resp.json(): + domains = host.get("domain_names", []) + if domain in domains: + return host.get("id") + return None + + async def create_proxy_host( api_url: str, npm_email: str, @@ -182,15 +205,32 @@ async def create_proxy_host( proxy_id = data.get("id") logger.info("Created NPM proxy host %s -> %s:%d (id=%s)", domain, forward_host, forward_port, proxy_id) - - # Step 2: Request SSL certificate and enable HTTPS - ssl_ok = await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email) - - return {"proxy_id": proxy_id, "ssl": ssl_ok} + elif resp.status_code == 400 and "already in use" in resp.text: + # Domain already exists — find and update the existing proxy host + proxy_id = await _find_proxy_host_by_domain(client, api_url, headers, domain) + if not proxy_id: + return {"error": f"Domain {domain} already in use but could not find existing proxy host"} + logger.info("Found existing NPM proxy host for %s (id=%s), updating...", domain, proxy_id) + update_resp = await client.put( + f"{api_url}/nginx/proxy-hosts/{proxy_id}", + json=payload, + headers=headers, + ) + if update_resp.status_code in (200, 201): + logger.info("Updated NPM proxy host %s -> %s:%d (id=%s)", + domain, forward_host, forward_port, proxy_id) + else: + logger.warning("Could not update proxy host %s: HTTP %s — %s", + proxy_id, update_resp.status_code, update_resp.text[:300]) else: error_msg = f"NPM returned {resp.status_code}: {resp.text[:300]}" logger.error("Failed to create proxy host: %s", error_msg) return {"error": error_msg} + + # Step 2: Request SSL certificate and enable HTTPS + ssl_ok = await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email) + + return {"proxy_id": proxy_id, "ssl": ssl_ok} except RuntimeError as exc: logger.error("NPM login failed: %s", exc) return {"error": f"NPM login failed: {exc}"} @@ -229,6 +269,32 @@ async def _request_ssl( logger.warning("No admin email set — skipping SSL certificate for %s", domain) return False + # Check if proxy host already has a valid certificate + try: + host_resp = await client.get(f"{api_url}/nginx/proxy-hosts/{proxy_id}", headers=headers) + if host_resp.status_code == 200: + host_data = host_resp.json() + existing_cert_id = host_data.get("certificate_id", 0) + if existing_cert_id and existing_cert_id > 0: + logger.info("Proxy host %s already has certificate (id=%s), ensuring SSL is enabled", + proxy_id, existing_cert_id) + ssl_update = { + "certificate_id": existing_cert_id, + "ssl_forced": True, + "hsts_enabled": True, + "http2_support": True, + } + update_resp = await client.put( + f"{api_url}/nginx/proxy-hosts/{proxy_id}", + json=ssl_update, + headers=headers, + ) + if update_resp.status_code in (200, 201): + logger.info("SSL enabled on existing proxy host %s (cert_id=%s)", proxy_id, existing_cert_id) + return True + except Exception as exc: + logger.warning("Could not check existing cert for proxy host %s: %s", proxy_id, exc) + ssl_payload = { "domain_names": [domain], "provider": "letsencrypt",