mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
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:
@@ -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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user