feat: items #9-13 — password change, webmail SSO, DKIM live, file manager security, cache busting

#9  auth.php: add self-service change-password action (current+new+confirm)
    accounts.php: fix admin change-password — accept account_id, fetch username
    for chpasswd (was using int ID), add Auth::require('admin') guard
    user.js: add Change Password page + navItem + submitChangePassword()

#10 EmailManager: store AES-256-CBC enc_password alongside SHA512-CRYPT hash
    webmail.php: rewrite login-url to use webmail_sso_tokens table
    novacpx-sso.php: Roundcube SSO bridge (validate token, decrypt, autosubmit)
    Migration 005: add enc_password column + webmail_sso_tokens table

#11 opendkim: installed, configured (/etc/opendkim.conf, signing.table,
    key.table, trusted.hosts), socket at /var/spool/postfix/opendkim/,
    Postfix milter wired, service enabled+running, key generation verified

#12 files.php: fix safe_path() for non-existent paths (write/mkdir),
    add safe_path_new() helper using parent-dir realpath check,
    fix delete guard (block deleting account root dirs),
    fix rename destination, clamp chmod to 0777

#13 nova.js: api() handles network errors, 429 rate-limit with retry-after,
    non-JSON responses (PHP fatal pages) — graceful error instead of throw
    admin/user/reseller index.php: filemtime-based cache-busting on all assets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 01:19:33 +00:00
parent 62707d62ce
commit 6fdccc6dbd
27 changed files with 1736 additions and 79 deletions
+24 -7
View File
@@ -8,14 +8,31 @@ window.Nova = (() => {
const { method = 'GET', body, params } = opts;
let url = `/api/${endpoint}/${action}`;
if (params) url += '?' + new URLSearchParams(params);
const res = await fetch(url, {
method,
credentials: 'include',
headers: body ? { 'Content-Type': 'application/json' } : {},
body: body ? JSON.stringify(body) : undefined,
});
let res;
try {
res = await fetch(url, {
method,
credentials: 'include',
headers: body ? { 'Content-Type': 'application/json' } : {},
body: body ? JSON.stringify(body) : undefined,
});
} catch (e) {
console.error(`Nova.api network error [${endpoint}/${action}]:`, e);
return { success: false, message: 'Network error — check your connection' };
}
if (res.status === 401) { location.href = '/?redirect=' + encodeURIComponent(location.pathname); return null; }
return res.json();
if (res.status === 429) {
const reset = res.headers.get('X-RateLimit-Reset');
const wait = reset ? Math.max(0, Math.ceil(Number(reset) - Date.now() / 1000)) : 60;
return { success: false, message: `Rate limited — try again in ${wait}s` };
}
const text = await res.text();
try {
return JSON.parse(text);
} catch {
console.error(`Nova.api non-JSON from [${endpoint}/${action}] (HTTP ${res.status}):`, text.slice(0, 500));
return { success: false, message: `Server error (HTTP ${res.status}) — see browser console` };
}
}
// ── Toast ─────────────────────────────────────────────────────────────────