Add Hallinta module with iRedMail email management

New "Hallinta" main tab (superadmin only) with "Sähköposti" sub-tab for
managing email via iRedAdmin-Pro REST API. Features:
- IRedMailClient PHP class with cookie-based session auth + auto-retry
- Domain CRUD (list, create, delete)
- Mailbox CRUD (list, create, delete, password change)
- Alias CRUD (list, create, delete)
- Configuration modal (API URL, admin credentials, connection test)
- Search/filter for mailboxes
- 13 new API endpoints, all requireSuperAdmin()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 19:58:10 +02:00
parent b3d8b7e067
commit 6ea62b075f
4 changed files with 908 additions and 0 deletions

327
script.js
View File

@@ -329,6 +329,10 @@ function switchToTab(target, subTab, extra) {
if (target === 'users') loadUsers();
if (target === 'settings') loadSettings();
if (target === 'companies') loadCompaniesTab();
if (target === 'hallinta') {
const hallintaSubMap = { email: 'hallinta-email' };
switchHallintaSubTab(hallintaSubMap[subTab] || 'hallinta-email');
}
}
document.querySelectorAll('.tab').forEach(tab => {
@@ -6739,6 +6743,9 @@ function applyModules(modules, hasIntegrations) {
}
}
});
// Hallinta-tabi: aina näkyvä superadmineille, ei moduuliriippuvuutta
const hallintaTab = document.getElementById('tab-hallinta');
if (hallintaTab) hallintaTab.style.display = isSuperAdmin ? '' : 'none';
// Jos aktiivinen tabi on piilotettu → vaihda ensimmäiseen näkyvään
const activeTab = document.querySelector('.tab.active');
if (activeTab && activeTab.style.display === 'none') {
@@ -6800,6 +6807,326 @@ async function loadBranding() {
}
}
// ==================== HALLINTA: IREDMAIL SÄHKÖPOSTI ====================
let iredmailCurrentDomain = '';
function switchHallintaSubTab(target) {
document.querySelectorAll('#hallinta-sub-tab-bar .sub-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('#tab-content-hallinta > .sub-tab-content').forEach(c => c.classList.remove('active'));
const btn = document.querySelector(`[data-hallinta-subtab="${target}"]`);
if (btn) btn.classList.add('active');
const content = document.getElementById('subtab-' + target);
if (content) content.classList.add('active');
if (target === 'hallinta-email') loadIRedMailDomains();
window.location.hash = 'hallinta/' + target.replace('hallinta-', '');
}
document.querySelectorAll('#hallinta-sub-tab-bar .sub-tab').forEach(btn => {
btn.addEventListener('click', () => switchHallintaSubTab(btn.dataset.hallintaSubtab));
});
// --- Domainit ---
async function loadIRedMailDomains() {
const tbody = document.getElementById('iredmail-domain-tbody');
const noData = document.getElementById('no-iredmail-domains');
const statusEl = document.getElementById('iredmail-status-text');
try {
const domains = await apiCall('iredmail_domains');
if (!domains || domains.length === 0) {
tbody.innerHTML = '';
noData.style.display = '';
statusEl.innerHTML = '⚠ Yhteys OK, mutta ei domaineja';
statusEl.parentElement.style.background = '#fff3cd';
return;
}
noData.style.display = 'none';
statusEl.innerHTML = '✓ Yhteys OK — ' + domains.length + ' domainia';
statusEl.parentElement.style.background = '#d4edda';
renderIRedMailDomains(domains);
} catch (e) {
tbody.innerHTML = '';
noData.style.display = '';
statusEl.innerHTML = '✗ ' + (e.message || 'Yhteysvirhe');
statusEl.parentElement.style.background = '#f8d7da';
}
}
function renderIRedMailDomains(domains) {
const tbody = document.getElementById('iredmail-domain-tbody');
tbody.innerHTML = domains.map(d => {
const domain = d.domain || d.primaryDomain || d.domainName || (typeof d === 'string' ? d : JSON.stringify(d));
const users = d.mailboxes || d.numberOfUsers || d.aliases_count || '';
const aliases = d.aliases || d.numberOfAliases || '';
const quota = d.maxQuotaSize || d.quota || '0';
return `<tr>
<td><a href="#" onclick="openIRedMailDomain('${esc(domain)}');return false;" style="font-weight:600;color:var(--primary-color);">${esc(domain)}</a></td>
<td>${esc(String(users))}</td>
<td>${esc(String(aliases))}</td>
<td>${esc(String(quota))}</td>
<td>
<button onclick="deleteIRedMailDomain('${esc(domain)}')" class="btn-danger" style="font-size:0.8rem;padding:3px 8px;">Poista</button>
</td>
</tr>`;
}).join('');
}
async function openIRedMailDomain(domain) {
iredmailCurrentDomain = domain;
document.getElementById('iredmail-domain-section').style.display = 'none';
document.getElementById('iredmail-users-section').style.display = '';
document.getElementById('iredmail-current-domain').textContent = domain;
document.getElementById('iredmail-user-domain-label').textContent = domain;
document.getElementById('iredmail-alias-domain-label').textContent = domain;
await Promise.all([loadIRedMailUsers(domain), loadIRedMailAliases(domain)]);
}
document.getElementById('iredmail-back-to-domains').addEventListener('click', (e) => {
e.preventDefault();
iredmailCurrentDomain = '';
document.getElementById('iredmail-users-section').style.display = 'none';
document.getElementById('iredmail-domain-section').style.display = '';
});
async function deleteIRedMailDomain(domain) {
if (!confirm('Poistetaanko domain ' + domain + ' ja KAIKKI sen tilit? Tätä ei voi perua!')) return;
try {
await apiCall('iredmail_domain_delete', 'POST', { domain });
await loadIRedMailDomains();
} catch (e) { alert(e.message); }
}
// Lisää domain
document.getElementById('btn-iredmail-add-domain').addEventListener('click', () => {
document.getElementById('iredmail-domain-name').value = '';
document.getElementById('iredmail-domain-cn').value = '';
document.getElementById('iredmail-domain-quota').value = '0';
document.getElementById('iredmail-domain-modal').style.display = 'flex';
});
document.getElementById('btn-iredmail-domain-save').addEventListener('click', async () => {
const domain = document.getElementById('iredmail-domain-name').value.trim();
if (!domain) { alert('Domain puuttuu'); return; }
try {
await apiCall('iredmail_domain_create', 'POST', {
domain,
cn: document.getElementById('iredmail-domain-cn').value.trim(),
quota: parseInt(document.getElementById('iredmail-domain-quota').value) || 0,
});
document.getElementById('iredmail-domain-modal').style.display = 'none';
await loadIRedMailDomains();
} catch (e) { alert(e.message); }
});
// --- Käyttäjät ---
async function loadIRedMailUsers(domain) {
const tbody = document.getElementById('iredmail-user-tbody');
try {
const users = await apiCall('iredmail_users&domain=' + encodeURIComponent(domain));
renderIRedMailUsers(users || []);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="5" style="color:#e74c3c;text-align:center;">${esc(e.message)}</td></tr>`;
}
}
function renderIRedMailUsers(users) {
const tbody = document.getElementById('iredmail-user-tbody');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#aaa;">Ei tilejä</td></tr>';
return;
}
tbody.innerHTML = users.map(u => {
const email = u.mail || u.email || u.username || '';
const name = u.cn || u.name || u.displayName || '';
const quota = u.mailQuota || u.quota || '0';
const status = u.accountStatus === 'disabled' ? '<span style="color:#e74c3c;">Ei käytössä</span>' : '<span style="color:#27ae60;">Aktiivinen</span>';
return `<tr data-email="${esc(email.toLowerCase())}">
<td style="font-weight:500;">${esc(email)}</td>
<td>${esc(name)}</td>
<td>${esc(String(quota))}</td>
<td>${status}</td>
<td>
<button onclick="showIRedMailPasswordModal('${esc(email)}')" class="btn-secondary" style="font-size:0.75rem;padding:2px 6px;">Salasana</button>
<button onclick="deleteIRedMailUser('${esc(email)}')" class="btn-danger" style="font-size:0.75rem;padding:2px 6px;">Poista</button>
</td>
</tr>`;
}).join('');
}
// Haku
document.getElementById('iredmail-user-search').addEventListener('input', function() {
const q = this.value.toLowerCase();
document.querySelectorAll('#iredmail-user-tbody tr[data-email]').forEach(row => {
const email = row.dataset.email || '';
const name = row.children[1]?.textContent?.toLowerCase() || '';
row.style.display = (email.includes(q) || name.includes(q)) ? '' : 'none';
});
});
// Lisää tili
document.getElementById('btn-iredmail-add-user').addEventListener('click', () => {
document.getElementById('iredmail-user-modal-title').textContent = 'Lisää tili';
document.getElementById('iredmail-user-local').value = '';
document.getElementById('iredmail-user-cn').value = '';
document.getElementById('iredmail-user-password').value = '';
document.getElementById('iredmail-user-quota').value = '1024';
document.getElementById('iredmail-user-modal').style.display = 'flex';
});
document.getElementById('btn-iredmail-user-save').addEventListener('click', async () => {
const local = document.getElementById('iredmail-user-local').value.trim();
const password = document.getElementById('iredmail-user-password').value;
if (!local) { alert('Käyttäjänimi puuttuu'); return; }
if (!password || password.length < 8) { alert('Salasana vähintään 8 merkkiä'); return; }
const email = local + '@' + iredmailCurrentDomain;
try {
await apiCall('iredmail_user_create', 'POST', {
email,
password,
cn: document.getElementById('iredmail-user-cn').value.trim(),
mailQuota: parseInt(document.getElementById('iredmail-user-quota').value) || 0,
});
document.getElementById('iredmail-user-modal').style.display = 'none';
await loadIRedMailUsers(iredmailCurrentDomain);
} catch (e) { alert(e.message); }
});
async function deleteIRedMailUser(email) {
if (!confirm('Poistetaanko tili ' + email + '? Kaikki viestit menetetään!')) return;
try {
await apiCall('iredmail_user_delete', 'POST', { email });
await loadIRedMailUsers(iredmailCurrentDomain);
} catch (e) { alert(e.message); }
}
// Salasanan vaihto
function showIRedMailPasswordModal(email) {
document.getElementById('iredmail-pw-email-label').textContent = email;
document.getElementById('iredmail-pw-new').value = '';
document.getElementById('iredmail-pw-confirm').value = '';
document.getElementById('iredmail-password-modal').style.display = 'flex';
document.getElementById('iredmail-password-modal').dataset.email = email;
}
document.getElementById('btn-iredmail-pw-save').addEventListener('click', async () => {
const pw1 = document.getElementById('iredmail-pw-new').value;
const pw2 = document.getElementById('iredmail-pw-confirm').value;
if (!pw1 || pw1.length < 8) { alert('Salasana vähintään 8 merkkiä'); return; }
if (pw1 !== pw2) { alert('Salasanat eivät täsmää'); return; }
const email = document.getElementById('iredmail-password-modal').dataset.email;
try {
await apiCall('iredmail_user_update', 'POST', { email, password: pw1 });
document.getElementById('iredmail-password-modal').style.display = 'none';
alert('Salasana vaihdettu!');
} catch (e) { alert(e.message); }
});
// --- Aliakset ---
async function loadIRedMailAliases(domain) {
const tbody = document.getElementById('iredmail-alias-tbody');
try {
const aliases = await apiCall('iredmail_aliases&domain=' + encodeURIComponent(domain));
renderIRedMailAliases(aliases || []);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="3" style="color:#e74c3c;text-align:center;">${esc(e.message)}</td></tr>`;
}
}
function renderIRedMailAliases(aliases) {
const tbody = document.getElementById('iredmail-alias-tbody');
if (aliases.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#aaa;">Ei aliaksia</td></tr>';
return;
}
tbody.innerHTML = aliases.map(a => {
const alias = a.mail || a.address || a.alias || '';
const members = a.members || a.goto || a.accessPolicy || '';
const membersStr = Array.isArray(members) ? members.join(', ') : String(members);
return `<tr>
<td style="font-weight:500;">${esc(alias)}</td>
<td style="font-size:0.85rem;color:#666;">${esc(membersStr)}</td>
<td>
<button onclick="deleteIRedMailAlias('${esc(alias)}')" class="btn-danger" style="font-size:0.75rem;padding:2px 6px;">Poista</button>
</td>
</tr>`;
}).join('');
}
document.getElementById('btn-iredmail-add-alias').addEventListener('click', () => {
document.getElementById('iredmail-alias-local').value = '';
document.getElementById('iredmail-alias-members').value = '';
document.getElementById('iredmail-alias-modal').style.display = 'flex';
});
document.getElementById('btn-iredmail-alias-save').addEventListener('click', async () => {
const local = document.getElementById('iredmail-alias-local').value.trim();
if (!local) { alert('Alias puuttuu'); return; }
const alias = local + '@' + iredmailCurrentDomain;
const members = document.getElementById('iredmail-alias-members').value.trim();
try {
await apiCall('iredmail_alias_create', 'POST', {
alias,
cn: alias,
members: members,
});
document.getElementById('iredmail-alias-modal').style.display = 'none';
await loadIRedMailAliases(iredmailCurrentDomain);
} catch (e) { alert(e.message); }
});
async function deleteIRedMailAlias(alias) {
if (!confirm('Poistetaanko alias ' + alias + '?')) return;
try {
await apiCall('iredmail_alias_delete', 'POST', { alias });
await loadIRedMailAliases(iredmailCurrentDomain);
} catch (e) { alert(e.message); }
}
// --- iRedMail asetukset ---
document.getElementById('btn-iredmail-settings').addEventListener('click', async () => {
try {
const cfg = await apiCall('iredmail_config');
document.getElementById('iredmail-cfg-url').value = cfg.url || '';
document.getElementById('iredmail-cfg-email').value = cfg.admin_email || '';
document.getElementById('iredmail-cfg-password').value = '';
document.getElementById('iredmail-cfg-password').placeholder = cfg.has_password ? 'Asetettu — jätä tyhjäksi jos ei muuteta' : 'Anna salasana';
document.getElementById('iredmail-cfg-status').innerHTML = '';
} catch (e) {
document.getElementById('iredmail-cfg-status').innerHTML = '<span style="color:#e74c3c;">' + esc(e.message) + '</span>';
}
document.getElementById('iredmail-config-modal').style.display = 'flex';
});
document.getElementById('btn-iredmail-cfg-save').addEventListener('click', async () => {
const data = {
url: document.getElementById('iredmail-cfg-url').value.trim(),
admin_email: document.getElementById('iredmail-cfg-email').value.trim(),
};
const pw = document.getElementById('iredmail-cfg-password').value;
if (pw) data.password = pw;
try {
await apiCall('iredmail_config_save', 'POST', data);
document.getElementById('iredmail-cfg-status').innerHTML = '<span style="color:#27ae60;">Tallennettu!</span>';
} catch (e) {
document.getElementById('iredmail-cfg-status').innerHTML = '<span style="color:#e74c3c;">' + esc(e.message) + '</span>';
}
});
document.getElementById('btn-iredmail-cfg-test').addEventListener('click', async () => {
const statusEl = document.getElementById('iredmail-cfg-status');
statusEl.innerHTML = '<span style="color:#888;">Testataan...</span>';
try {
const result = await apiCall('iredmail_test', 'POST', {});
statusEl.innerHTML = '<span style="color:#27ae60;">✓ Yhteys OK! ' + (result.domains || 0) + ' domainia.</span>';
} catch (e) {
statusEl.innerHTML = '<span style="color:#e74c3c;">✗ ' + esc(e.message) + '</span>';
}
});
// Init — branding ensin, sitten auth (luo session-cookien), sitten captcha (käyttää samaa sessiota)
loadBranding().then(async () => {
await checkAuth();