feat(ucp): NexusPBX v2.5.0 Portail Utilisateur isolé (Refs #4)
This commit is contained in:
1 parent
7922e338e3
commit
72e43f1a9a
18 files changed
+3499
-62
No files matched your search
+251
-2
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user