This commit is contained in:
2026-02-07 21:13:50 +01:00
parent 3d8ab57f31
commit a18df0018c
11 changed files with 157 additions and 67 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View File

@@ -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
) )

View File

@@ -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,

View File

@@ -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)

View File

@@ -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.

View File

@@ -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">

View File

@@ -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);

View File

@@ -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,

View File

@@ -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()