Convert iRedMail from REST API to direct MySQL (vmail DB) + per-company integration

- Replace IRedMailClient REST API class with direct MySQL/PDO connection to vmail database
- Move iRedMail config from global config table to per-company integrations (like Zammad)
- Add iRedMail integration card to API settings with DB host/name/user/password/port fields
- Add iRedMail checkbox to integrations section in company settings
- Change Hallinta tab visibility: show for admins (not just superadmins) when module enabled
- API endpoints now use requireCompany() + requireSuperAdmin() and get config from integrations
- Password hashing uses SSHA512 (iRedMail default)
- Mask db_password in API responses (like token masking for Zammad)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 21:31:23 +02:00
parent 50f34ac37b
commit f24123be81
3 changed files with 302 additions and 233 deletions

112
script.js
View File

@@ -2767,10 +2767,22 @@ async function loadSettings() {
const zammadInteg = integs.find(i => i.type === 'zammad');
const zammadEnabled = zammadInteg?.enabled;
// Saatavuus-API kortti näkyy aina (perus API-asetukset)
const iredmailInteg = integs.find(i => i.type === 'iredmail');
const iredmailEnabled = iredmailInteg?.enabled;
const teleCard = document.getElementById('settings-telegram-card');
const zammadCard = document.getElementById('settings-zammad-card');
const iredmailCard = document.getElementById('settings-iredmail-card');
if (teleCard) teleCard.style.display = telegramEnabled ? '' : 'none';
if (zammadCard) zammadCard.style.display = zammadEnabled ? '' : 'none';
if (iredmailCard) iredmailCard.style.display = iredmailEnabled ? '' : 'none';
// Lataa iRedMail-asetukset korttiin
if (iredmailEnabled && iredmailInteg?.config) {
document.getElementById('company-iredmail-host').value = iredmailInteg.config.db_host || '';
document.getElementById('company-iredmail-dbname').value = iredmailInteg.config.db_name || 'vmail';
document.getElementById('company-iredmail-port').value = iredmailInteg.config.db_port || '3306';
document.getElementById('company-iredmail-user').value = iredmailInteg.config.db_user || '';
document.getElementById('company-iredmail-password').value = iredmailInteg.config.db_password || '';
}
// Lataa Zammad-asetukset korttiin
if (zammadEnabled && zammadInteg?.config) {
document.getElementById('company-zammad-url').value = zammadInteg.config.url || '';
@@ -3025,7 +3037,7 @@ async function loadCompanyIntegrations() {
try {
const integrations = await apiCall('integrations');
// Aseta vain checkboxit — konfiguraatio ladataan API-tabissa
['zammad', 'saatavuus_api', 'telegram'].forEach(type => {
['zammad', 'saatavuus_api', 'telegram', 'iredmail'].forEach(type => {
const integ = integrations.find(i => i.type === type);
const cb = document.querySelector(`#integrations-checkboxes input[data-integration="${type}"]`);
if (cb) cb.checked = integ?.enabled || false;
@@ -3082,12 +3094,62 @@ document.querySelector('#integrations-checkboxes input[data-integration="saatavu
document.querySelector('#integrations-checkboxes input[data-integration="telegram"]')?.addEventListener('change', async function() {
try {
await saveSimpleIntegration('telegram', this.checked);
// Päivitä API-sivun kortti
const card = document.getElementById('settings-telegram-card');
if (card) card.style.display = this.checked ? '' : 'none';
} catch (e) { console.error(e); }
});
// iRedMail checkbox toggle
document.querySelector('#integrations-checkboxes input[data-integration="iredmail"]')?.addEventListener('change', async function() {
try {
await saveSimpleIntegration('iredmail', this.checked);
const card = document.getElementById('settings-iredmail-card');
if (card) card.style.display = this.checked ? '' : 'none';
} catch (e) { console.error(e); }
});
// iRedMail tallenna
async function saveCompanyIRedMail() {
const config = {
db_host: document.getElementById('company-iredmail-host').value.trim(),
db_name: document.getElementById('company-iredmail-dbname').value.trim() || 'vmail',
db_port: parseInt(document.getElementById('company-iredmail-port').value) || 3306,
db_user: document.getElementById('company-iredmail-user').value.trim(),
db_password: document.getElementById('company-iredmail-password').value,
};
await apiCall('integration_save', 'POST', { type: 'iredmail', enabled: true, config });
}
document.getElementById('btn-company-iredmail-save')?.addEventListener('click', async () => {
const result = document.getElementById('company-iredmail-result');
try {
await saveCompanyIRedMail();
result.style.display = 'block';
result.style.background = '#d4edda';
result.textContent = '✅ Tallennettu!';
} catch (e) {
result.style.display = 'block';
result.style.background = '#f8d7da';
result.textContent = '❌ ' + e.message;
}
});
document.getElementById('btn-company-iredmail-test')?.addEventListener('click', async () => {
const result = document.getElementById('company-iredmail-result');
result.style.display = 'block';
result.style.background = '#f8f9fb';
result.textContent = 'Testataan...';
try {
await saveCompanyIRedMail();
const res = await apiCall('integration_test', 'POST', { type: 'iredmail' });
result.style.background = '#d4edda';
result.textContent = `✅ Yhteys OK! ${res.domains || 0} domainia.`;
} catch (e) {
result.style.background = '#f8d7da';
result.textContent = '❌ ' + e.message;
}
});
// Lataa ryhmät
document.getElementById('btn-company-zammad-groups')?.addEventListener('click', async () => {
// Tallenna ensin URL ja token
@@ -6742,9 +6804,9 @@ function applyModules(modules, hasIntegrations) {
if (mod === 'settings') {
const showSettings = enabled.includes(mod) && isAdminUser && (isSuperAdmin || hasIntegrations === true);
tabBtn.style.display = showSettings ? '' : 'none';
// hallinta: vain superadmineille + pitää olla moduulina päällä
// hallinta: adminille/superadminille + pitää olla moduulina päällä
} else if (mod === 'hallinta') {
tabBtn.style.display = (enabled.includes(mod) && isSuperAdmin) ? '' : 'none';
tabBtn.style.display = (enabled.includes(mod) && isAdminUser) ? '' : 'none';
} else {
tabBtn.style.display = enabled.includes(mod) ? '' : 'none';
}
@@ -7089,48 +7151,6 @@ async function deleteIRedMailAlias(alias) {
} 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();