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:
327
script.js
327
script.js
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user