Add SSL certificate mode: Let's Encrypt or Wildcard per NPM

Settings > NPM Integration now allows choosing between per-customer
Let's Encrypt certificates (default) or a shared wildcard certificate
already uploaded in NPM. Includes backend, frontend UI, and i18n support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:01:28 +01:00
parent 3d28f13054
commit c7fc4758e3
12 changed files with 274 additions and 7 deletions

View File

@@ -381,6 +381,32 @@
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('cfg-npm-api-password')"><i class="bi bi-eye"></i></button>
</div>
</div>
<!-- SSL Certificate Mode -->
<div class="col-12 mt-3">
<hr class="my-2">
<h6 class="mb-2" data-i18n="settings.sslModeTitle">SSL Certificate Mode</h6>
</div>
<div class="col-md-8">
<label class="form-label" data-i18n="settings.sslMode">SSL Mode</label>
<select class="form-select" id="cfg-ssl-mode" onchange="onSslModeChange()">
<option value="letsencrypt" data-i18n="settings.sslModeLetsencrypt">Per-Customer Let's Encrypt Certificate</option>
<option value="wildcard" data-i18n="settings.sslModeWildcard">Wildcard Certificate (pre-configured in NPM)</option>
</select>
<div class="form-text" data-i18n="settings.sslModeHint">Choose how SSL certificates are assigned to customer proxy hosts.</div>
</div>
<div class="col-md-8" id="wildcard-cert-section" style="display:none;">
<label class="form-label" data-i18n="settings.wildcardCertificate">Wildcard Certificate</label>
<div class="input-group">
<select class="form-select" id="cfg-wildcard-cert-id">
<option value="" data-i18n="settings.selectCertificate">-- Select a certificate --</option>
</select>
<button type="button" class="btn btn-outline-secondary" onclick="loadNpmCertificates()" title="Refresh">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="form-text" data-i18n="settings.wildcardCertHint">Select the wildcard certificate (e.g. *.example.com) already uploaded in NPM.</div>
<div id="wildcard-cert-status" class="mt-1"></div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary me-2"><i class="bi bi-save me-1"></i><span data-i18n="settings.saveNpmSettings">Save NPM Settings</span></button>

View File

@@ -816,6 +816,14 @@ async function loadSettings() {
document.getElementById('cfg-dashboard-base-port').value = cfg.dashboard_base_port || 9000;
document.getElementById('cfg-npm-api-url').value = cfg.npm_api_url || '';
document.getElementById('npm-credentials-status').textContent = cfg.npm_credentials_set ? t('settings.credentialsSet') : t('settings.noCredentials');
// SSL mode
document.getElementById('cfg-ssl-mode').value = cfg.ssl_mode || 'letsencrypt';
onSslModeChange();
if (cfg.ssl_mode === 'wildcard') {
loadNpmCertificates(cfg.wildcard_cert_id);
}
document.getElementById('cfg-mgmt-image').value = cfg.netbird_management_image || '';
document.getElementById('cfg-signal-image').value = cfg.netbird_signal_image || '';
document.getElementById('cfg-relay-image').value = cfg.netbird_relay_image || '';
@@ -876,6 +884,14 @@ document.getElementById('settings-npm-form').addEventListener('submit', async (e
const password = document.getElementById('cfg-npm-api-password').value;
if (email) payload.npm_api_email = email;
if (password) payload.npm_api_password = password;
// SSL mode
const sslMode = document.getElementById('cfg-ssl-mode').value;
payload.ssl_mode = sslMode;
if (sslMode === 'wildcard') {
const certId = document.getElementById('cfg-wildcard-cert-id').value;
if (certId) payload.wildcard_cert_id = parseInt(certId);
}
try {
await api('PUT', '/settings/system', payload);
showSettingsAlert('success', t('messages.npmSettingsSaved'));
@@ -924,6 +940,42 @@ async function testNpmConnection() {
}
}
// SSL mode toggle
function onSslModeChange() {
const mode = document.getElementById('cfg-ssl-mode').value;
const section = document.getElementById('wildcard-cert-section');
section.style.display = mode === 'wildcard' ? '' : 'none';
}
// Load NPM wildcard certificates into dropdown
async function loadNpmCertificates(preselectId) {
const select = document.getElementById('cfg-wildcard-cert-id');
const statusEl = document.getElementById('wildcard-cert-status');
select.innerHTML = `<option value="">${t('settings.selectCertificate')}</option>`;
statusEl.textContent = t('common.loading');
statusEl.className = 'mt-1 text-muted';
try {
const certs = await api('GET', '/settings/npm-certificates');
const wildcards = certs.filter(c => c.is_wildcard || (c.domain_names && c.domain_names.some(d => d.startsWith('*.'))));
wildcards.forEach(c => {
const domains = (c.domain_names || []).join(', ');
const expires = c.expires_on ? ` (${t('settings.expiresOn')}: ${new Date(c.expires_on).toLocaleDateString()})` : '';
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = `${domains}${expires}`;
select.appendChild(opt);
});
if (preselectId) select.value = preselectId;
statusEl.textContent = t('settings.certsLoaded', { count: wildcards.length });
statusEl.className = wildcards.length > 0 ? 'mt-1 text-success small' : 'mt-1 text-warning small';
if (wildcards.length === 0) statusEl.textContent = t('settings.noWildcardCerts');
} catch (err) {
statusEl.textContent = t('errors.failed', { error: err.message });
statusEl.className = 'mt-1 text-danger small';
}
}
// Change password form
document.getElementById('change-password-form').addEventListener('submit', async (e) => {
e.preventDefault();

View File

@@ -147,6 +147,17 @@
"noCredentials": "Keine NPM-Zugangsdaten konfiguriert",
"saveNpmSettings": "NPM Einstellungen speichern",
"testConnection": "Verbindung testen",
"sslModeTitle": "SSL-Zertifikat Modus",
"sslMode": "SSL Modus",
"sslModeLetsencrypt": "Let's Encrypt (pro Kunde)",
"sslModeWildcard": "Wildcard-Zertifikat",
"sslModeHint": "Waehlen Sie, ob jeder Kunde ein eigenes Let's Encrypt Zertifikat erhaelt oder ein gemeinsames Wildcard-Zertifikat verwendet wird.",
"wildcardCertificate": "Wildcard-Zertifikat",
"selectCertificate": "-- Zertifikat waehlen --",
"wildcardCertHint": "Waehlen Sie das Wildcard-Zertifikat (z.B. *.example.com), das bereits in NPM hochgeladen ist.",
"noWildcardCerts": "Keine Wildcard-Zertifikate in NPM gefunden.",
"certsLoaded": "{count} Wildcard-Zertifikat(e) gefunden.",
"expiresOn": "Ablaufdatum",
"managementImage": "Management Image",
"managementImagePlaceholder": "netbirdio/management:latest",
"signalImage": "Signal Image",

View File

@@ -147,6 +147,17 @@
"noCredentials": "No NPM credentials configured",
"saveNpmSettings": "Save NPM Settings",
"testConnection": "Test Connection",
"sslModeTitle": "SSL Certificate Mode",
"sslMode": "SSL Mode",
"sslModeLetsencrypt": "Let's Encrypt (per customer)",
"sslModeWildcard": "Wildcard Certificate",
"sslModeHint": "Choose whether each customer gets an individual Let's Encrypt certificate or uses a shared wildcard certificate.",
"wildcardCertificate": "Wildcard Certificate",
"selectCertificate": "-- Select certificate --",
"wildcardCertHint": "Select the wildcard certificate (e.g. *.example.com) already uploaded in NPM.",
"noWildcardCerts": "No wildcard certificates found in NPM.",
"certsLoaded": "{count} wildcard certificate(s) found.",
"expiresOn": "Expires",
"managementImage": "Management Image",
"managementImagePlaceholder": "netbirdio/management:latest",
"signalImage": "Signal Image",