feat(#25): email notifications via CyberMail

- Notifier.php: CyberMail API sender with 4 trigger types (account
  created, suspended, disk quota warning, SSL expiry)
- Reads cybermail_api_key / notify_from_* / notify_admin_email from
  settings table
- accounts.php: fires Notifier on create (welcome + admin alert) and
  suspend (user + admin alert)
- system.php: notify-settings GET, save-notify-settings POST,
  test-notify POST (with API key masking)
- bin/notify-checks.php: daily cron for disk ≥85% and SSL ≤14 days
  (flag-based dedup in settings table)
- admin panel: Notifications page with form + trigger reference table;
  sidebar link added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 04:12:47 +00:00
parent 33c36ffc65
commit 2ab74b7569
6 changed files with 407 additions and 2 deletions
+83
View File
@@ -99,6 +99,7 @@
backups,
cloudflare,
'server-options': serverOptions,
notifications,
settings,
};
@@ -480,6 +481,88 @@
</div>`;
}
// ── Notifications (#25) ───────────────────────────────────────────────────
async function notifications() {
const res = await Nova.api('system', 'notify-settings');
const s = res?.data || {};
return `
<div class="page-header"><h2 class="page-title">Email Notifications</h2></div>
<div class="card mb-2">
<div class="card-header"><span class="card-title">CyberMail Settings</span></div>
<div class="card-body">
<form id="notify-form">
<div class="grid-2">
<div class="form-group" style="grid-column:1/-1">
<label>CyberMail API Key</label>
<input type="password" id="nf-apikey" name="cybermail_api_key" class="form-control" placeholder="${s.cybermail_api_key_masked || 'sk_live_…'}" value="">
<span class="form-hint">Leave blank to keep existing key. Get your key from platform.cyberpersons.com</span>
</div>
<div class="form-group">
<label>From Email</label>
<input type="email" name="notify_from_email" class="form-control" value="${s.notify_from_email || ''}">
</div>
<div class="form-group">
<label>From Name</label>
<input type="text" name="notify_from_name" class="form-control" value="${s.notify_from_name || 'NovaCPX Panel'}">
</div>
<div class="form-group">
<label>Admin Alert Email</label>
<input type="email" name="notify_admin_email" class="form-control" value="${s.notify_admin_email || ''}">
<span class="form-hint">Receives alerts for new accounts, suspensions, disk warnings</span>
</div>
<div class="form-group">
<label>Notifications</label>
<select name="notifications_enabled" class="form-control">
<option value="1" ${(s.notifications_enabled ?? '1') !== '0' ? 'selected' : ''}>Enabled</option>
<option value="0" ${s.notifications_enabled === '0' ? 'selected' : ''}>Disabled</option>
</select>
</div>
</div>
<div style="display:flex;gap:.5rem;align-items:center">
<button type="submit" class="btn btn-primary">Save Settings</button>
<button type="button" class="btn btn-ghost" onclick="notifyTest()">Send Test Email</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">Notification Triggers</span></div>
<div class="card-body">
<table class="table">
<thead><tr><th>Event</th><th>Recipient</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>Account Created</td><td>New user + Admin</td><td>Sends welcome email with credentials</td></tr>
<tr><td>Account Suspended</td><td>Account holder + Admin</td><td>Includes suspension reason</td></tr>
<tr><td>Disk Quota ≥85%</td><td>Account holder + Admin</td><td>Once per day per account (cron)</td></tr>
<tr><td>SSL Expiry ≤14 days</td><td>Account holder + Admin</td><td>Once per threshold per domain (cron)</td></tr>
</tbody>
</table>
<p class="text-muted text-sm" style="margin-top:.5rem">Disk quota and SSL expiry checks run daily via cron.</p>
</div>
</div>`;
}
document.addEventListener('submit', async e => {
if (!e.target.matches('#notify-form')) return;
e.preventDefault();
const fd = new FormData(e.target);
const body = Object.fromEntries(fd.entries());
if (!body.cybermail_api_key) delete body.cybermail_api_key;
const res = await Nova.api('system', 'save-notify-settings', { method: 'POST', body });
if (res?.success) Nova.toast('Notification settings saved', 'success');
else Nova.toast(res?.message || 'Save failed', 'error');
});
window.notifyTest = async () => {
const email = prompt('Send test email to:');
if (!email) return;
const res = await Nova.api('system', 'test-notify', { method: 'POST', body: { to: email } });
if (res?.success) Nova.toast(res.message, 'success');
else Nova.toast(res?.message || 'Send failed', 'error');
};
// ── Settings ───────────────────────────────────────────────────────────────
async function settings() {
return `