bugfix
This commit is contained in:
@@ -128,7 +128,8 @@ class SystemConfig(Base):
|
|||||||
base_domain: Mapped[str] = mapped_column(String(255), nullable=False)
|
base_domain: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
admin_email: Mapped[str] = mapped_column(String(255), nullable=False)
|
admin_email: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
npm_api_url: Mapped[str] = mapped_column(String(500), nullable=False)
|
npm_api_url: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||||
npm_api_token_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
npm_api_email_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
npm_api_password_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
netbird_management_image: Mapped[str] = mapped_column(
|
netbird_management_image: Mapped[str] = mapped_column(
|
||||||
String(255), default="netbirdio/management:latest"
|
String(255), default="netbirdio/management:latest"
|
||||||
)
|
)
|
||||||
@@ -154,12 +155,12 @@ class SystemConfig(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Serialize config to dictionary (token masked)."""
|
"""Serialize config to dictionary (credentials masked)."""
|
||||||
return {
|
return {
|
||||||
"base_domain": self.base_domain,
|
"base_domain": self.base_domain,
|
||||||
"admin_email": self.admin_email,
|
"admin_email": self.admin_email,
|
||||||
"npm_api_url": self.npm_api_url,
|
"npm_api_url": self.npm_api_url,
|
||||||
"npm_api_token_set": bool(self.npm_api_token_encrypted),
|
"npm_credentials_set": bool(self.npm_api_email_encrypted and self.npm_api_password_encrypted),
|
||||||
"netbird_management_image": self.netbird_management_image,
|
"netbird_management_image": self.netbird_management_image,
|
||||||
"netbird_signal_image": self.netbird_signal_image,
|
"netbird_signal_image": self.netbird_signal_image,
|
||||||
"netbird_relay_image": self.netbird_relay_image,
|
"netbird_relay_image": self.netbird_relay_image,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async def update_settings(
|
|||||||
):
|
):
|
||||||
"""Update system configuration values.
|
"""Update system configuration values.
|
||||||
|
|
||||||
Only provided (non-None) fields are updated. The NPM API token is
|
Only provided (non-None) fields are updated. NPM credentials are
|
||||||
encrypted before storage.
|
encrypted before storage.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -67,10 +67,13 @@ async def update_settings(
|
|||||||
|
|
||||||
update_data = payload.model_dump(exclude_none=True)
|
update_data = payload.model_dump(exclude_none=True)
|
||||||
|
|
||||||
# Handle NPM token encryption
|
# Handle NPM credentials encryption
|
||||||
if "npm_api_token" in update_data:
|
if "npm_api_email" in update_data:
|
||||||
raw_token = update_data.pop("npm_api_token")
|
raw_email = update_data.pop("npm_api_email")
|
||||||
row.npm_api_token_encrypted = encrypt_value(raw_token)
|
row.npm_api_email_encrypted = encrypt_value(raw_email)
|
||||||
|
if "npm_api_password" in update_data:
|
||||||
|
raw_password = update_data.pop("npm_api_password")
|
||||||
|
row.npm_api_password_encrypted = encrypt_value(raw_password)
|
||||||
|
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
if hasattr(row, field):
|
if hasattr(row, field):
|
||||||
@@ -103,11 +106,13 @@ async def test_npm(
|
|||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="System configuration not initialized.",
|
detail="System configuration not initialized.",
|
||||||
)
|
)
|
||||||
if not config.npm_api_url or not config.npm_api_token:
|
if not config.npm_api_url or not config.npm_api_email or not config.npm_api_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="NPM API URL or token not configured.",
|
detail="NPM API URL or credentials not configured.",
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await npm_service.test_npm_connection(config.npm_api_url, config.npm_api_token)
|
result = await npm_service.test_npm_connection(
|
||||||
|
config.npm_api_url, config.npm_api_email, config.npm_api_password
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
|
|||||||
dashboard_container = f"netbird-kunde{customer_id}-dashboard"
|
dashboard_container = f"netbird-kunde{customer_id}-dashboard"
|
||||||
npm_result = await npm_service.create_proxy_host(
|
npm_result = await npm_service.create_proxy_host(
|
||||||
api_url=config.npm_api_url,
|
api_url=config.npm_api_url,
|
||||||
api_token=config.npm_api_token,
|
npm_email=config.npm_api_email,
|
||||||
|
npm_password=config.npm_api_password,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
forward_host=dashboard_container,
|
forward_host=dashboard_container,
|
||||||
forward_port=80,
|
forward_port=80,
|
||||||
@@ -260,10 +261,11 @@ async def undeploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
|
|||||||
_log_action(db, customer_id, "undeploy", "error", f"Container removal error: {exc}")
|
_log_action(db, customer_id, "undeploy", "error", f"Container removal error: {exc}")
|
||||||
|
|
||||||
# Remove NPM proxy host
|
# Remove NPM proxy host
|
||||||
if deployment.npm_proxy_id and config.npm_api_token:
|
if deployment.npm_proxy_id and config.npm_api_email:
|
||||||
try:
|
try:
|
||||||
await npm_service.delete_proxy_host(
|
await npm_service.delete_proxy_host(
|
||||||
config.npm_api_url, config.npm_api_token, deployment.npm_proxy_id
|
config.npm_api_url, config.npm_api_email, config.npm_api_password,
|
||||||
|
deployment.npm_proxy_id,
|
||||||
)
|
)
|
||||||
_log_action(db, customer_id, "undeploy", "info", "NPM proxy host removed.")
|
_log_action(db, customer_id, "undeploy", "info", "NPM proxy host removed.")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
"""Nginx Proxy Manager API integration.
|
"""Nginx Proxy Manager API integration.
|
||||||
|
|
||||||
|
NPM uses JWT authentication — there are no static API tokens.
|
||||||
|
Every API session starts with a login (POST /api/tokens) using email + password,
|
||||||
|
which returns a short-lived JWT. That JWT is then used as Bearer token for all
|
||||||
|
subsequent requests.
|
||||||
|
|
||||||
Creates, updates, and deletes proxy host entries so each customer's NetBird
|
Creates, updates, and deletes proxy host entries so each customer's NetBird
|
||||||
dashboard is accessible at ``{subdomain}.{base_domain}`` with automatic
|
dashboard is accessible at ``{subdomain}.{base_domain}`` with automatic
|
||||||
Let's Encrypt SSL certificates.
|
Let's Encrypt SSL certificates.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -16,29 +21,67 @@ logger = logging.getLogger(__name__)
|
|||||||
NPM_TIMEOUT = 30
|
NPM_TIMEOUT = 30
|
||||||
|
|
||||||
|
|
||||||
async def test_npm_connection(api_url: str, api_token: str) -> dict[str, Any]:
|
async def _npm_login(client: httpx.AsyncClient, api_url: str, email: str, password: str) -> str:
|
||||||
"""Test connectivity to the Nginx Proxy Manager API.
|
"""Authenticate with NPM and return a JWT token.
|
||||||
|
|
||||||
|
NPM does NOT support static API keys. Auth is always:
|
||||||
|
POST /api/tokens with {"identity": "<email>", "secret": "<password>"}
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
client: httpx async client.
|
||||||
api_url: NPM API base URL (e.g. ``http://npm:81/api``).
|
api_url: NPM API base URL (e.g. ``http://npm:81/api``).
|
||||||
api_token: Bearer token for authentication.
|
email: NPM login email / identity.
|
||||||
|
password: NPM login password / secret.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JWT token string.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If login fails.
|
||||||
|
"""
|
||||||
|
resp = await client.post(
|
||||||
|
f"{api_url}/tokens",
|
||||||
|
json={"identity": email, "secret": password},
|
||||||
|
)
|
||||||
|
if resp.status_code in (200, 201):
|
||||||
|
data = resp.json()
|
||||||
|
token = data.get("token")
|
||||||
|
if token:
|
||||||
|
logger.debug("NPM login successful for %s", email)
|
||||||
|
return token
|
||||||
|
raise RuntimeError("NPM login response did not contain a token.")
|
||||||
|
raise RuntimeError(
|
||||||
|
f"NPM login failed (HTTP {resp.status_code}): {resp.text[:300]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_npm_connection(api_url: str, email: str, password: str) -> dict[str, Any]:
|
||||||
|
"""Test connectivity to NPM by logging in and listing proxy hosts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_url: NPM API base URL.
|
||||||
|
email: NPM login email.
|
||||||
|
password: NPM login password.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with ``ok`` (bool) and ``message`` (str).
|
Dict with ``ok`` (bool) and ``message`` (str).
|
||||||
"""
|
"""
|
||||||
headers = {"Authorization": f"Bearer {api_token}"}
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
||||||
|
token = await _npm_login(client, api_url, email, password)
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
resp = await client.get(f"{api_url}/nginx/proxy-hosts", headers=headers)
|
resp = await client.get(f"{api_url}/nginx/proxy-hosts", headers=headers)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
count = len(resp.json())
|
count = len(resp.json())
|
||||||
return {"ok": True, "message": f"Connected. {count} proxy hosts found."}
|
return {"ok": True, "message": f"Connected. Login OK. {count} proxy hosts found."}
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"message": f"NPM returned status {resp.status_code}: {resp.text[:200]}",
|
"message": f"Login OK but listing hosts returned {resp.status_code}: {resp.text[:200]}",
|
||||||
}
|
}
|
||||||
|
except RuntimeError as exc:
|
||||||
|
return {"ok": False, "message": str(exc)}
|
||||||
except httpx.ConnectError:
|
except httpx.ConnectError:
|
||||||
return {"ok": False, "message": "Connection refused. Is NPM running?"}
|
return {"ok": False, "message": "Connection refused. Is NPM running and reachable?"}
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
return {"ok": False, "message": "Connection timed out."}
|
return {"ok": False, "message": "Connection timed out."}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -47,7 +90,8 @@ async def test_npm_connection(api_url: str, api_token: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
async def create_proxy_host(
|
async def create_proxy_host(
|
||||||
api_url: str,
|
api_url: str,
|
||||||
api_token: str,
|
npm_email: str,
|
||||||
|
npm_password: str,
|
||||||
domain: str,
|
domain: str,
|
||||||
forward_host: str,
|
forward_host: str,
|
||||||
forward_port: int = 80,
|
forward_port: int = 80,
|
||||||
@@ -57,15 +101,13 @@ async def create_proxy_host(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Create a proxy host entry in NPM with SSL for a customer.
|
"""Create a proxy host entry in NPM with SSL for a customer.
|
||||||
|
|
||||||
The proxy routes traffic as follows:
|
Logs in first to get a JWT, then creates the proxy host with advanced
|
||||||
- ``/`` -> dashboard container (port 80)
|
routing config for management, signal, and relay containers.
|
||||||
- ``/api`` -> management container (port 80)
|
|
||||||
- ``/signalexchange.*`` -> signal container (port 80)
|
|
||||||
- ``/relay`` -> relay container (port 80)
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
api_url: NPM API base URL.
|
api_url: NPM API base URL.
|
||||||
api_token: Bearer token.
|
npm_email: NPM login email.
|
||||||
|
npm_password: NPM login password.
|
||||||
domain: Full domain (e.g. ``kunde1.example.com``).
|
domain: Full domain (e.g. ``kunde1.example.com``).
|
||||||
forward_host: Container name for the dashboard.
|
forward_host: Container name for the dashboard.
|
||||||
forward_port: Port to forward to (default 80).
|
forward_port: Port to forward to (default 80).
|
||||||
@@ -76,11 +118,6 @@ async def create_proxy_host(
|
|||||||
Returns:
|
Returns:
|
||||||
Dict with ``proxy_id`` on success or ``error`` on failure.
|
Dict with ``proxy_id`` on success or ``error`` on failure.
|
||||||
"""
|
"""
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build advanced Nginx config to route sub-paths to different containers
|
# Build advanced Nginx config to route sub-paths to different containers
|
||||||
mgmt_container = f"netbird-kunde{customer_id}-management"
|
mgmt_container = f"netbird-kunde{customer_id}-management"
|
||||||
signal_container = f"netbird-kunde{customer_id}-signal"
|
signal_container = f"netbird-kunde{customer_id}-signal"
|
||||||
@@ -136,6 +173,14 @@ location /relay {{
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
||||||
|
# Step 1: Login to NPM
|
||||||
|
token = await _npm_login(client, api_url, npm_email, npm_password)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 2: Create proxy host
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
f"{api_url}/nginx/proxy-hosts", json=payload, headers=headers
|
f"{api_url}/nginx/proxy-hosts", json=payload, headers=headers
|
||||||
)
|
)
|
||||||
@@ -144,7 +189,7 @@ location /relay {{
|
|||||||
proxy_id = data.get("id")
|
proxy_id = data.get("id")
|
||||||
logger.info("Created NPM proxy host %s (id=%s)", domain, proxy_id)
|
logger.info("Created NPM proxy host %s (id=%s)", domain, proxy_id)
|
||||||
|
|
||||||
# Request SSL certificate
|
# Step 3: Request SSL certificate
|
||||||
await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email)
|
await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email)
|
||||||
|
|
||||||
return {"proxy_id": proxy_id}
|
return {"proxy_id": proxy_id}
|
||||||
@@ -152,6 +197,9 @@ location /relay {{
|
|||||||
error_msg = f"NPM returned {resp.status_code}: {resp.text[:300]}"
|
error_msg = f"NPM returned {resp.status_code}: {resp.text[:300]}"
|
||||||
logger.error("Failed to create proxy host: %s", error_msg)
|
logger.error("Failed to create proxy host: %s", error_msg)
|
||||||
return {"error": error_msg}
|
return {"error": error_msg}
|
||||||
|
except RuntimeError as exc:
|
||||||
|
logger.error("NPM login failed: %s", exc)
|
||||||
|
return {"error": f"NPM login failed: {exc}"}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("NPM API error: %s", exc)
|
logger.error("NPM API error: %s", exc)
|
||||||
return {"error": str(exc)}
|
return {"error": str(exc)}
|
||||||
@@ -168,9 +216,9 @@ async def _request_ssl(
|
|||||||
"""Request a Let's Encrypt SSL certificate for a proxy host.
|
"""Request a Let's Encrypt SSL certificate for a proxy host.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: httpx client.
|
client: httpx client (already authenticated).
|
||||||
api_url: NPM API base URL.
|
api_url: NPM API base URL.
|
||||||
headers: Auth headers.
|
headers: Auth headers with Bearer token.
|
||||||
proxy_id: The proxy host ID.
|
proxy_id: The proxy host ID.
|
||||||
domain: The domain to certify.
|
domain: The domain to certify.
|
||||||
admin_email: Contact email for LE.
|
admin_email: Contact email for LE.
|
||||||
@@ -203,21 +251,25 @@ async def _request_ssl(
|
|||||||
|
|
||||||
|
|
||||||
async def delete_proxy_host(
|
async def delete_proxy_host(
|
||||||
api_url: str, api_token: str, proxy_id: int
|
api_url: str, npm_email: str, npm_password: str, proxy_id: int
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Delete a proxy host from NPM.
|
"""Delete a proxy host from NPM.
|
||||||
|
|
||||||
|
Logs in first to get a fresh JWT, then deletes the proxy host.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
api_url: NPM API base URL.
|
api_url: NPM API base URL.
|
||||||
api_token: Bearer token.
|
npm_email: NPM login email.
|
||||||
|
npm_password: NPM login password.
|
||||||
proxy_id: The proxy host ID to delete.
|
proxy_id: The proxy host ID to delete.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True on success.
|
True on success.
|
||||||
"""
|
"""
|
||||||
headers = {"Authorization": f"Bearer {api_token}"}
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
||||||
|
token = await _npm_login(client, api_url, npm_email, npm_password)
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
resp = await client.delete(
|
resp = await client.delete(
|
||||||
f"{api_url}/nginx/proxy-hosts/{proxy_id}", headers=headers
|
f"{api_url}/nginx/proxy-hosts/{proxy_id}", headers=headers
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class AppConfig:
|
|||||||
base_domain: str
|
base_domain: str
|
||||||
admin_email: str
|
admin_email: str
|
||||||
npm_api_url: str
|
npm_api_url: str
|
||||||
npm_api_token: str # decrypted
|
npm_api_email: str # decrypted — NPM login email
|
||||||
|
npm_api_password: str # decrypted — NPM login password
|
||||||
netbird_management_image: str
|
netbird_management_image: str
|
||||||
netbird_signal_image: str
|
netbird_signal_image: str
|
||||||
netbird_relay_image: str
|
netbird_relay_image: str
|
||||||
@@ -55,15 +56,20 @@ def get_system_config(db: Session) -> Optional[AppConfig]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
npm_token = decrypt_value(row.npm_api_token_encrypted)
|
npm_email = decrypt_value(row.npm_api_email_encrypted)
|
||||||
except Exception:
|
except Exception:
|
||||||
npm_token = ""
|
npm_email = ""
|
||||||
|
try:
|
||||||
|
npm_password = decrypt_value(row.npm_api_password_encrypted)
|
||||||
|
except Exception:
|
||||||
|
npm_password = ""
|
||||||
|
|
||||||
return AppConfig(
|
return AppConfig(
|
||||||
base_domain=row.base_domain,
|
base_domain=row.base_domain,
|
||||||
admin_email=row.admin_email,
|
admin_email=row.admin_email,
|
||||||
npm_api_url=row.npm_api_url,
|
npm_api_url=row.npm_api_url,
|
||||||
npm_api_token=npm_token,
|
npm_api_email=npm_email,
|
||||||
|
npm_api_password=npm_password,
|
||||||
netbird_management_image=row.netbird_management_image,
|
netbird_management_image=row.netbird_management_image,
|
||||||
netbird_signal_image=row.netbird_signal_image,
|
netbird_signal_image=row.netbird_signal_image,
|
||||||
netbird_relay_image=row.netbird_relay_image,
|
netbird_relay_image=row.netbird_relay_image,
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ class SystemConfigUpdate(BaseModel):
|
|||||||
base_domain: Optional[str] = Field(None, min_length=1, max_length=255)
|
base_domain: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||||
admin_email: Optional[str] = Field(None, max_length=255)
|
admin_email: Optional[str] = Field(None, max_length=255)
|
||||||
npm_api_url: Optional[str] = Field(None, max_length=500)
|
npm_api_url: Optional[str] = Field(None, max_length=500)
|
||||||
npm_api_token: Optional[str] = None # plaintext, will be encrypted before storage
|
npm_api_email: Optional[str] = Field(None, max_length=255) # NPM login email
|
||||||
|
npm_api_password: Optional[str] = None # NPM login password, encrypted before storage
|
||||||
netbird_management_image: Optional[str] = Field(None, max_length=255)
|
netbird_management_image: Optional[str] = Field(None, max_length=255)
|
||||||
netbird_signal_image: Optional[str] = Field(None, max_length=255)
|
netbird_signal_image: Optional[str] = Field(None, max_length=255)
|
||||||
netbird_relay_image: Optional[str] = Field(None, max_length=255)
|
netbird_relay_image: Optional[str] = Field(None, max_length=255)
|
||||||
|
|||||||
34
install.sh
34
install.sh
@@ -202,6 +202,9 @@ echo -e "${BLUE}${BOLD}[Step 5/10]${NC} ${BLUE}Nginx Proxy Manager Configuration
|
|||||||
echo -e "${CYAN}NetBird MSP needs to integrate with Nginx Proxy Manager (NPM).${NC}\n"
|
echo -e "${CYAN}NetBird MSP needs to integrate with Nginx Proxy Manager (NPM).${NC}\n"
|
||||||
|
|
||||||
# NPM API URL
|
# NPM API URL
|
||||||
|
echo -e "${YELLOW}NPM uses JWT authentication (email + password login).${NC}"
|
||||||
|
echo -e "${YELLOW}There are no static API keys — the system logs in automatically.${NC}\n"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -p "NPM API URL [http://nginx-proxy-manager:81/api]: " NPM_API_URL
|
read -p "NPM API URL [http://nginx-proxy-manager:81/api]: " NPM_API_URL
|
||||||
NPM_API_URL=${NPM_API_URL:-http://nginx-proxy-manager:81/api}
|
NPM_API_URL=${NPM_API_URL:-http://nginx-proxy-manager:81/api}
|
||||||
@@ -212,19 +215,25 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# NPM API Token
|
# NPM Login Email
|
||||||
echo -e "\n${YELLOW}To get your NPM API Token:${NC}"
|
echo ""
|
||||||
echo -e " 1. Login to Nginx Proxy Manager"
|
|
||||||
echo -e " 2. Go to Users → Your User"
|
|
||||||
echo -e " 3. Copy the API Token\n"
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -sp "NPM API Token: " NPM_API_TOKEN
|
read -p "NPM Login Email (your NPM admin email): " NPM_EMAIL
|
||||||
echo ""
|
if [[ "$NPM_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
||||||
if [ ${#NPM_API_TOKEN} -ge 20 ]; then
|
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
echo -e "${RED}Token seems too short. Please enter the complete token.${NC}"
|
echo -e "${RED}Invalid email address.${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# NPM Login Password
|
||||||
|
while true; do
|
||||||
|
read -sp "NPM Login Password: " NPM_PASSWORD
|
||||||
|
echo ""
|
||||||
|
if [ ${#NPM_PASSWORD} -ge 1 ]; then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo -e "${RED}Password cannot be empty.${NC}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -296,6 +305,7 @@ echo -e " Admin Username: ${GREEN}$ADMIN_USERNAME${NC}"
|
|||||||
echo -e " Admin Email: ${GREEN}$ADMIN_EMAIL${NC}"
|
echo -e " Admin Email: ${GREEN}$ADMIN_EMAIL${NC}"
|
||||||
echo -e " Base Domain: ${GREEN}$BASE_DOMAIN${NC}"
|
echo -e " Base Domain: ${GREEN}$BASE_DOMAIN${NC}"
|
||||||
echo -e " NPM API URL: ${GREEN}$NPM_API_URL${NC}"
|
echo -e " NPM API URL: ${GREEN}$NPM_API_URL${NC}"
|
||||||
|
echo -e " NPM Login: ${GREEN}$NPM_EMAIL${NC}"
|
||||||
echo -e " Data Directory: ${GREEN}$DATA_DIR${NC}"
|
echo -e " Data Directory: ${GREEN}$DATA_DIR${NC}"
|
||||||
echo -e " Install Dir: ${GREEN}$INSTALL_DIR${NC}\n"
|
echo -e " Install Dir: ${GREEN}$INSTALL_DIR${NC}\n"
|
||||||
|
|
||||||
@@ -408,7 +418,8 @@ if not existing_config:
|
|||||||
base_domain='$BASE_DOMAIN',
|
base_domain='$BASE_DOMAIN',
|
||||||
admin_email='$ADMIN_EMAIL',
|
admin_email='$ADMIN_EMAIL',
|
||||||
npm_api_url='$NPM_API_URL',
|
npm_api_url='$NPM_API_URL',
|
||||||
npm_api_token_encrypted=encrypt_value('$NPM_API_TOKEN'),
|
npm_api_email_encrypted=encrypt_value('$NPM_EMAIL'),
|
||||||
|
npm_api_password_encrypted=encrypt_value('$NPM_PASSWORD'),
|
||||||
netbird_management_image='$NETBIRD_MANAGEMENT_IMAGE',
|
netbird_management_image='$NETBIRD_MANAGEMENT_IMAGE',
|
||||||
netbird_signal_image='$NETBIRD_SIGNAL_IMAGE',
|
netbird_signal_image='$NETBIRD_SIGNAL_IMAGE',
|
||||||
netbird_relay_image='$NETBIRD_RELAY_IMAGE',
|
netbird_relay_image='$NETBIRD_RELAY_IMAGE',
|
||||||
@@ -517,6 +528,7 @@ Admin Username: $ADMIN_USERNAME
|
|||||||
Admin Email: $ADMIN_EMAIL
|
Admin Email: $ADMIN_EMAIL
|
||||||
Base Domain: $BASE_DOMAIN
|
Base Domain: $BASE_DOMAIN
|
||||||
NPM API URL: $NPM_API_URL
|
NPM API URL: $NPM_API_URL
|
||||||
|
NPM Login: $NPM_EMAIL
|
||||||
Data Directory: $DATA_DIR
|
Data Directory: $DATA_DIR
|
||||||
|
|
||||||
NOTE: All settings are stored in the database and editable via Web UI.
|
NOTE: All settings are stored in the database and editable via Web UI.
|
||||||
|
|||||||
@@ -295,18 +295,24 @@
|
|||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="settings-npm-form">
|
<form id="settings-npm-form">
|
||||||
|
<p class="text-muted mb-3">NPM uses JWT authentication. Enter your NPM login credentials (email + password). The system will automatically log in and obtain tokens for API calls.</p>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">NPM API URL</label>
|
<label class="form-label">NPM API URL</label>
|
||||||
<input type="url" class="form-control" id="cfg-npm-api-url" placeholder="http://nginx-proxy-manager:81/api">
|
<input type="text" class="form-control" id="cfg-npm-api-url" placeholder="http://nginx-proxy-manager:81/api">
|
||||||
|
<div class="form-text">http:// or https:// - must include /api at the end</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">NPM API Token</label>
|
<label class="form-label">NPM Login Email</label>
|
||||||
|
<input type="text" class="form-control" id="cfg-npm-api-email" placeholder="Leave empty to keep current">
|
||||||
|
<div class="form-text" id="npm-credentials-status"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">NPM Login Password</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" class="form-control" id="cfg-npm-api-token" placeholder="Leave empty to keep current token">
|
<input type="password" class="form-control" id="cfg-npm-api-password" placeholder="Leave empty to keep current">
|
||||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('cfg-npm-api-token')"><i class="bi bi-eye"></i></button>
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('cfg-npm-api-password')"><i class="bi bi-eye"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text" id="npm-token-status"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ async function loadSettings() {
|
|||||||
document.getElementById('cfg-docker-network').value = cfg.docker_network || '';
|
document.getElementById('cfg-docker-network').value = cfg.docker_network || '';
|
||||||
document.getElementById('cfg-relay-base-port').value = cfg.relay_base_port || 3478;
|
document.getElementById('cfg-relay-base-port').value = cfg.relay_base_port || 3478;
|
||||||
document.getElementById('cfg-npm-api-url').value = cfg.npm_api_url || '';
|
document.getElementById('cfg-npm-api-url').value = cfg.npm_api_url || '';
|
||||||
document.getElementById('npm-token-status').textContent = cfg.npm_api_token_set ? 'Token is set (leave empty to keep current)' : 'No token configured';
|
document.getElementById('npm-credentials-status').textContent = cfg.npm_credentials_set ? 'Credentials are set (leave empty to keep current)' : 'No NPM credentials configured';
|
||||||
document.getElementById('cfg-mgmt-image').value = cfg.netbird_management_image || '';
|
document.getElementById('cfg-mgmt-image').value = cfg.netbird_management_image || '';
|
||||||
document.getElementById('cfg-signal-image').value = cfg.netbird_signal_image || '';
|
document.getElementById('cfg-signal-image').value = cfg.netbird_signal_image || '';
|
||||||
document.getElementById('cfg-relay-image').value = cfg.netbird_relay_image || '';
|
document.getElementById('cfg-relay-image').value = cfg.netbird_relay_image || '';
|
||||||
@@ -482,12 +482,15 @@ document.getElementById('settings-system-form').addEventListener('submit', async
|
|||||||
document.getElementById('settings-npm-form').addEventListener('submit', async (e) => {
|
document.getElementById('settings-npm-form').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const payload = { npm_api_url: document.getElementById('cfg-npm-api-url').value };
|
const payload = { npm_api_url: document.getElementById('cfg-npm-api-url').value };
|
||||||
const token = document.getElementById('cfg-npm-api-token').value;
|
const email = document.getElementById('cfg-npm-api-email').value;
|
||||||
if (token) payload.npm_api_token = token;
|
const password = document.getElementById('cfg-npm-api-password').value;
|
||||||
|
if (email) payload.npm_api_email = email;
|
||||||
|
if (password) payload.npm_api_password = password;
|
||||||
try {
|
try {
|
||||||
await api('PUT', '/settings/system', payload);
|
await api('PUT', '/settings/system', payload);
|
||||||
showSettingsAlert('success', 'NPM settings saved.');
|
showSettingsAlert('success', 'NPM settings saved.');
|
||||||
document.getElementById('cfg-npm-api-token').value = '';
|
document.getElementById('cfg-npm-api-email').value = '';
|
||||||
|
document.getElementById('cfg-npm-api-password').value = '';
|
||||||
loadSettings();
|
loadSettings();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showSettingsAlert('danger', 'Failed: ' + err.message);
|
showSettingsAlert('danger', 'Failed: ' + err.message);
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ def db_session():
|
|||||||
base_domain="test.example.com",
|
base_domain="test.example.com",
|
||||||
admin_email="admin@test.com",
|
admin_email="admin@test.com",
|
||||||
npm_api_url="http://localhost:81/api",
|
npm_api_url="http://localhost:81/api",
|
||||||
npm_api_token_encrypted=encrypt_value("test-npm-token"),
|
npm_api_email_encrypted=encrypt_value("admin@npm.local"),
|
||||||
|
npm_api_password_encrypted=encrypt_value("test-npm-password"),
|
||||||
data_dir="/tmp/netbird-test",
|
data_dir="/tmp/netbird-test",
|
||||||
docker_network="test-network",
|
docker_network="test-network",
|
||||||
relay_base_port=3478,
|
relay_base_port=3478,
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ def test_db():
|
|||||||
base_domain="test.example.com",
|
base_domain="test.example.com",
|
||||||
admin_email="admin@test.com",
|
admin_email="admin@test.com",
|
||||||
npm_api_url="http://localhost:81/api",
|
npm_api_url="http://localhost:81/api",
|
||||||
npm_api_token_encrypted=encrypt_value("test-npm-token"),
|
npm_api_email_encrypted=encrypt_value("admin@npm.local"),
|
||||||
|
npm_api_password_encrypted=encrypt_value("test-npm-password"),
|
||||||
)
|
)
|
||||||
session.add(config)
|
session.add(config)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user