mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
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:
@@ -154,6 +154,10 @@ $_v = fn($f) => '?v=' . @filemtime(dirname(__DIR__) . $f);
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||||
Server Options
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="notifications">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
Notifications
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
Settings
|
||||
|
||||
@@ -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 `
|
||||
|
||||
Reference in New Issue
Block a user