feat(ucp): NexusPBX v2.5.0 Portail Utilisateur isolé (Refs #4)

This commit is contained in:
sorcier committed 2026-11-11 18:47:25 +01:00
1 parent 7922e338e3
commit 72e43f1a9a
18 files changed
+3499 -62

No files matched your search

+251 -2
View File
@@ -211,6 +211,7 @@ function renderAll() {
renderFeatureCodes();
renderSipSettings();
renderCertificates();
renderUcpTab();
renderSettingsAndDiagnostics();
}
@@ -356,6 +357,8 @@ function renderSummaryCards() {
if (pagingBadge) pagingBadge.textContent = (appState.paging_groups || []).length;
const pbBadge = document.getElementById('nav-badge-phonebook');
if (pbBadge) pbBadge.textContent = (appState.phonebook_contacts || []).length;
const ucpBadge = document.getElementById('nav-badge-ucp');
if (ucpBadge) ucpBadge.textContent = (appState.ucp_users || []).length;
document.getElementById('nav-badge-trunks').textContent = trks.length;
const obBadge = document.getElementById('nav-badge-outbound');
if (obBadge) obBadge.textContent = obRoutes.length;
@@ -3629,6 +3632,7 @@ const PAGE_TITLES = {
system: 'Paramètres Globaux, Diagnostic Debian & Fichiers .conf',
backups: 'Sauvegardes & Restauration Système (Disaster Recovery)',
firewall: 'Pare-feu Réseau & Sécurité Téléphonie (Fail2ban & NFTables)',
ucp: 'Portail Utilisateur (UCP) — Comptes & Droits',
cluster: 'Haute Disponibilité & Redondance (Cluster HA Keepalived)'
};
@@ -3709,6 +3713,11 @@ function switchTab(tabId) {
loadCluster();
}
// Si onglet Portail Utilisateur (UCP), actualiser l'affichage
if (tabId === 'ucp') {
renderUcpTab();
}
toggleMobileSidebar(false);
}
@@ -4346,19 +4355,29 @@ document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('btn-save-firewall-settings');
if (btn) btn.disabled = true;
const rawUcpHttps = f.elements['ucp_https_port']?.value;
const rawUcpHttp = f.elements['ucp_http_port']?.value;
const payload = {
firewall_enabled: f.elements['firewall_enabled']?.checked ? 1 : 0,
admin_https_port: Number(f.elements['admin_https_port']?.value || 8443),
certbot_strict_port80: f.elements['certbot_strict_port80']?.checked ? 1 : 0,
fail2ban_sip_maxretry: Number(f.elements['fail2ban_sip_maxretry']?.value || 3),
fail2ban_sip_bantime: Number(f.elements['fail2ban_sip_bantime']?.value || 3600),
fail2ban_whitelist_ips: (f.elements['fail2ban_whitelist_ips']?.value || '').trim()
fail2ban_whitelist_ips: (f.elements['fail2ban_whitelist_ips']?.value || '').trim(),
ucp_enabled: f.elements['ucp_enabled']?.checked ? 1 : 0,
ucp_https_port: rawUcpHttps !== '' && rawUcpHttps !== undefined ? Number(rawUcpHttps) : 8444,
ucp_http_port: rawUcpHttp !== '' && rawUcpHttp !== undefined ? Number(rawUcpHttp) : 8084,
fail2ban_ucp_maxretry: Number(f.elements['fail2ban_ucp_maxretry']?.value || 3),
fail2ban_ucp_bantime: Number(f.elements['fail2ban_ucp_bantime']?.value || 3600)
};
try {
const res = await apiCall('save_firewall_settings', payload);
showToast(res.message, 'success');
await loadFirewallStatus();
if (typeof loadState === 'function') {
await loadState();
}
} catch (err) {
showToast("Erreur application sécurité : " + err.message, 'error');
} finally {
@@ -5379,7 +5398,12 @@ function renderFirewall() {
`;
} else {
tbody.innerHTML = banned.map(b => {
const jailClass = b.jail === 'asterisk' ? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30' : 'bg-purple-500/20 text-purple-300 border border-purple-500/30';
let jailClass = 'bg-purple-500/20 text-purple-300 border border-purple-500/30';
if (b.jail === 'asterisk') {
jailClass = 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30';
} else if (b.jail === 'nexus-ucp') {
jailClass = 'bg-sky-500/20 text-sky-300 border border-sky-500/30';
}
return `
<tr class="hover:bg-anthracite-750 transition">
<td class="px-4 py-3 font-mono font-bold text-white text-sm">
@@ -5424,6 +5448,21 @@ function renderFirewall() {
if (form.elements['fail2ban_whitelist_ips'] && document.activeElement !== form.elements['fail2ban_whitelist_ips']) {
form.elements['fail2ban_whitelist_ips'].value = s.fail2ban_whitelist_ips || '';
}
if (form.elements['ucp_enabled']) {
form.elements['ucp_enabled'].checked = Number(s.ucp_enabled ?? 1) === 1;
}
if (form.elements['ucp_https_port'] && document.activeElement !== form.elements['ucp_https_port']) {
form.elements['ucp_https_port'].value = s.ucp_https_port !== undefined ? s.ucp_https_port : 8444;
}
if (form.elements['ucp_http_port'] && document.activeElement !== form.elements['ucp_http_port']) {
form.elements['ucp_http_port'].value = s.ucp_http_port !== undefined ? s.ucp_http_port : 8084;
}
if (form.elements['fail2ban_ucp_maxretry']) {
form.elements['fail2ban_ucp_maxretry'].value = s.fail2ban_ucp_maxretry || 3;
}
if (form.elements['fail2ban_ucp_bantime']) {
form.elements['fail2ban_ucp_bantime'].value = s.fail2ban_ucp_bantime || 3600;
}
}
}
@@ -6798,4 +6837,214 @@ function copyClusterSecret() {
});
}
// =========================================================================
// GESTION DES COMPTES PORTAIL UTILISATEUR (UCP)
// =========================================================================
function renderUcpTab() {
const s = appState.settings || {};
const ucpEnabled = Number(s.ucp_enabled ?? 1) === 1;
const httpsPort = Number(s.ucp_https_port ?? 8444);
const httpPort = Number(s.ucp_http_port ?? 8084);
const host = window.location.hostname || 'localhost';
const statusBadge = document.getElementById('ucp-service-status-badge');
if (statusBadge) {
if (!ucpEnabled || (httpsPort <= 0 && httpPort <= 0)) {
statusBadge.textContent = 'Portail désactivé';
statusBadge.className = 'px-2 py-0.5 rounded-full text-[11px] font-semibold bg-rose-500/15 text-rose-300 border border-rose-500/30';
} else {
statusBadge.textContent = 'En ligne & Isolé';
statusBadge.className = 'px-2 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-500/15 text-emerald-300 border border-emerald-500/30';
}
}
const urlsBox = document.getElementById('ucp-access-urls');
if (urlsBox) {
if (!ucpEnabled) {
urlsBox.innerHTML = '<span class="text-slate-500 italic">Activez l’UCP dans Pare-feu & Sécurité pour exposer le portail.</span>';
} else {
const links = [];
if (httpsPort > 0) {
const url = `https://${host}:${httpsPort}/`;
links.push(`<a href="${escapeHtml(url)}" target="_blank" rel="noopener" class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-mono transition">🔒 ${escapeHtml(url)}</a>`);
}
if (httpPort > 0) {
const url = `http://${host}:${httpPort}/`;
links.push(`<a href="${escapeHtml(url)}" target="_blank" rel="noopener" class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-sky-500/10 hover:bg-sky-500/20 text-sky-300 border border-sky-500/30 font-mono transition">🌐 ${escapeHtml(url)} (Reverse Proxy)</a>`);
}
urlsBox.innerHTML = links.length ? links.join('') : '<span class="text-amber-400">Aucun port d’écoute configuré.</span>';
}
}
renderUcpUsersTable();
}
function renderUcpUsersTable() {
const tbody = document.getElementById('ucp-users-table-body');
if (!tbody) return;
const users = appState.ucp_users || [];
const extMap = new Map((appState.extensions || []).map(e => [String(e.extension), e]));
if (users.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="px-5 py-10 text-center text-slate-400">
<div class="flex flex-col items-center gap-2">
<span class="text-sm font-medium text-slate-300">Aucun utilisateur UCP créé pour le moment.</span>
<span class="text-xs text-slate-500">Cliquez sur « Nouvel Utilisateur UCP » pour ouvrir un accès employé à sa messagerie vocale, ses renvois et l’annuaire.</span>
</div>
</td>
</tr>
`;
return;
}
tbody.innerHTML = users.map(u => {
const exts = Array.isArray(u.extensions) ? u.extensions : [];
const extChips = exts.length
? exts.map(num => {
const info = extMap.get(String(num));
const title = info ? `${num} — ${info.caller_id_name || ''}` : String(num);
return `<span title="${escapeHtml(title)}" class="px-2 py-0.5 rounded-md bg-sky-500/15 text-sky-300 border border-sky-500/30 font-mono text-[11px] font-semibold">${escapeHtml(String(num))}</span>`;
}).join(' ')
: '<span class="text-slate-500 italic">Aucune extension</span>';
const isEnabled = Number(u.enabled) === 1;
const statusPill = isEnabled
? '<span class="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-500/15 text-emerald-300 border border-emerald-500/30">Actif</span>'
: '<span class="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-rose-500/15 text-rose-300 border border-rose-500/30">Désactivé</span>';
const lastLogin = u.last_login_at ? escapeHtml(u.last_login_at) : '<span class="text-slate-500">Jamais connecté</span>';
return `
<tr class="hover:bg-anthracite-800/40 transition">
<td class="px-5 py-3.5 font-mono font-bold text-white">${escapeHtml(u.username || '')}</td>
<td class="px-5 py-3.5">
<div class="font-semibold text-slate-200">${escapeHtml(u.display_name || u.username || '')}</div>
<div class="text-[11px] text-slate-400">${u.email ? escapeHtml(u.email) : '<span class="text-slate-600">—</span>'}</div>
</td>
<td class="px-5 py-3.5">
<div class="flex flex-wrap gap-1.5">${extChips}</div>
</td>
<td class="px-5 py-3.5">${statusPill}</td>
<td class="px-5 py-3.5 text-slate-400">${lastLogin}</td>
<td class="px-5 py-3.5 text-right whitespace-nowrap">
<button type="button" onclick="openUcpUserModal(${Number(u.id)})"
class="px-2.5 py-1 rounded-lg bg-anthracite-800 hover:bg-sky-600/20 text-slate-300 hover:text-sky-300 border border-anthracite-700 hover:border-sky-500/40 text-xs font-semibold transition mr-1.5">
Modifier
</button>
<button type="button" onclick="deleteUcpUser(${Number(u.id)}, '${escapeHtml(u.username || '')}')"
class="px-2.5 py-1 rounded-lg bg-rose-500/10 hover:bg-rose-600 text-rose-300 hover:text-white border border-rose-500/30 text-xs font-semibold transition">
Supprimer
</button>
</td>
</tr>
`;
}).join('');
}
function openUcpUserModal(userId = 0) {
const modal = document.getElementById('modal-ucp-user');
if (!modal) return;
const users = appState.ucp_users || [];
const user = userId ? users.find(u => Number(u.id) === Number(userId)) : null;
document.getElementById('ucp-user-id').value = user ? Number(user.id) : 0;
document.getElementById('ucp-modal-title').textContent = user ? `Modifier l'utilisateur UCP (${user.username})` : 'Nouvel utilisateur UCP';
document.getElementById('ucp-user-username').value = user ? (user.username || '') : '';
document.getElementById('ucp-user-display-name').value = user ? (user.display_name || '') : '';
document.getElementById('ucp-user-email').value = user ? (user.email || '') : '';
document.getElementById('ucp-user-password').value = '';
document.getElementById('ucp-user-password').required = !user;
document.getElementById('ucp-password-hint').textContent = user ? '(laisser vide pour conserver)' : '(min. 6 car., requis)';
document.getElementById('ucp-user-enabled').checked = user ? Number(user.enabled) === 1 : true;
const selectedExts = new Set((user?.extensions || []).map(String));
const container = document.getElementById('ucp-user-extensions-container');
const exts = [...(appState.extensions || [])].sort((a, b) => Number(a.extension) - Number(b.extension));
if (container) {
if (exts.length === 0) {
container.innerHTML = '<div class="col-span-full text-xs text-slate-500 py-2">Aucune extension SIP configurée sur le PBX.</div>';
} else {
container.innerHTML = exts.map(ext => {
const num = String(ext.extension);
const checked = selectedExts.has(num) ? 'checked' : '';
return `
<label class="flex items-center gap-2.5 p-2 rounded-lg bg-anthracite-900 border border-anthracite-800 hover:border-sky-500/40 cursor-pointer select-none transition">
<input type="checkbox" name="ucp_ext_choice" value="${escapeHtml(num)}" ${checked}
class="w-4 h-4 rounded border-anthracite-600 bg-anthracite-950 text-sky-500 focus:ring-sky-500">
<div class="min-w-0">
<div class="text-xs font-bold text-white font-mono">${escapeHtml(num)}</div>
<div class="text-[11px] text-slate-400 truncate">${escapeHtml(ext.caller_id_name || 'Sans nom')}</div>
</div>
</label>
`;
}).join('');
}
}
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeUcpUserModal() {
const modal = document.getElementById('modal-ucp-user');
if (!modal) return;
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function saveUcpUser(event) {
event.preventDefault();
const btn = document.getElementById('btn-save-ucp-user');
if (btn) btn.disabled = true;
const checkedExts = Array.from(document.querySelectorAll('#ucp-user-extensions-container input[name="ucp_ext_choice"]:checked'))
.map(el => el.value);
const payload = {
id: Number(document.getElementById('ucp-user-id').value || 0),
username: (document.getElementById('ucp-user-username').value || '').trim(),
display_name: (document.getElementById('ucp-user-display-name').value || '').trim(),
email: (document.getElementById('ucp-user-email').value || '').trim(),
password: document.getElementById('ucp-user-password').value || '',
enabled: document.getElementById('ucp-user-enabled').checked ? 1 : 0,
extensions: checkedExts
};
try {
const res = await apiCall('save_ucp_user', payload);
appState.ucp_users = res.ucp_users || [];
renderUcpTab();
const ucpBadge = document.getElementById('nav-badge-ucp');
if (ucpBadge) ucpBadge.textContent = appState.ucp_users.length;
closeUcpUserModal();
showToast(res.message || 'Utilisateur UCP enregistré.', 'success');
} catch (err) {
showToast(err.message || "Erreur lors de l'enregistrement de l'utilisateur UCP.", 'error');
} finally {
if (btn) btn.disabled = false;
}
}
async function deleteUcpUser(userId, username) {
if (!confirm(`Supprimer définitivement le compte UCP « ${username} » ?`)) {
return;
}
try {
const res = await apiCall('delete_ucp_user', { id: Number(userId) });
appState.ucp_users = res.ucp_users || [];
renderUcpTab();
const ucpBadge = document.getElementById('nav-badge-ucp');
if (ucpBadge) ucpBadge.textContent = appState.ucp_users.length;
showToast(res.message || 'Utilisateur UCP supprimé.', 'success');
} catch (err) {
showToast(err.message || "Erreur lors de la suppression de l'utilisateur UCP.", 'error');
}
}