Harden session handling, add password reset/change, and firm up round polling
Session hardening: / and /admin now respond with Cache-Control: no-store, and
both pages re-derive their auth state on pageshow (event.persisted) as a
safety net against bfcache showing a stale logged-in/out view across
back/forward navigation. The user page also syncs logout across tabs via the
storage event, since localStorage is shared but in-memory JS state isn't.
Password recovery: admin gets a "Reset" button per user (POST
/admin/users/{id}/reset-password) that generates and sets a new password,
shown once — passwords are Argon2-hashed and can never be recovered, only
replaced. Users get self-service password change (POST
/users/me/change-password, requires the current password) under a new
Profilo tab, alongside read-only account info (username, address, balance,
join date).
Round display robustness: the user dashboard now refreshes immediately on
tab visibility change (background tabs get their timers throttled hard),
shows an explicit "connessione persa" state after repeated failed polls
instead of silently freezing on stale data, and polls faster both right when
the countdown hits zero and through the gap where the round is past its
deadline but still waiting for in-flight bets to confirm before the server
actually closes it.
This commit is contained in:
+48
-6
@@ -271,17 +271,17 @@
|
||||
|
||||
<div class="view" id="view-utenti">
|
||||
<h2 class="section-title">Utenti</h2>
|
||||
<p class="hint">Elenco utenti registrati, con saldo interno e accesso alla chiave privata per interventi manuali (es. restituire fondi bloccati).</p>
|
||||
<p class="hint">Elenco utenti registrati, con saldo interno, accesso alla chiave privata per interventi manuali (es. restituire fondi bloccati) e reset password per chi resta bloccato fuori dall'account.</p>
|
||||
|
||||
<div class="warning-banner">
|
||||
⚠ La chiave privata dà accesso completo ai fondi dell'utente. Ogni volta che la visualizzi viene registrata nell'audit log del server. Non condividerla, non salvarla altrove.
|
||||
⚠ La chiave privata dà accesso completo ai fondi dell'utente: ogni visualizzazione viene registrata nell'audit log, non condividerla né salvarla altrove. La password esistente di un utente non è mai recuperabile (è salvata solo come hash Argon2) — "Reset" ne genera una nuova al posto della vecchia, anche questo audit-loggato.
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Username</th><th>Indirizzo</th><th>Saldo (PLM)</th><th>Registrato</th><th>Chiave</th></tr>
|
||||
<tr><th>ID</th><th>Username</th><th>Indirizzo</th><th>Saldo (PLM)</th><th>Registrato</th><th>Chiave</th><th>Password</th></tr>
|
||||
</thead>
|
||||
<tbody id="users-tbody"></tbody>
|
||||
</table>
|
||||
@@ -568,8 +568,12 @@ async function loadUsers() {
|
||||
<button class="reveal" onclick="revealPrivkey(${u.id}, this)">Mostra</button>
|
||||
<div class="privkey-box hidden" id="privkey-${u.id}"></div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="secondary" style="width:auto;margin-top:0;min-height:30px;padding:4px 10px;font-size:0.78rem" onclick="resetUserPassword(${u.id}, this)">Reset</button>
|
||||
<div class="privkey-box hidden" id="newpass-${u.id}"></div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="6" class="hint">Nessun utente registrato.</td></tr>';
|
||||
`).join('') || '<tr><td colspan="7" class="hint">Nessun utente registrato.</td></tr>';
|
||||
} catch (e) {
|
||||
toast('Errore nel caricamento utenti: ' + e.message, 'error');
|
||||
}
|
||||
@@ -598,6 +602,26 @@ async function revealPrivkey(userId, button) {
|
||||
});
|
||||
}
|
||||
|
||||
async function resetUserPassword(userId, button) {
|
||||
if (!window.confirm(
|
||||
"Verrà generata una nuova password casuale per questo utente, che non potrà più accedere con quella vecchia. " +
|
||||
"L'azione viene registrata nell'audit log. Continuare?"
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
const box = document.getElementById('newpass-' + userId);
|
||||
await withLoading(button, '…', async () => {
|
||||
try {
|
||||
const data = await callAdmin('POST', '/admin/users/' + userId + '/reset-password');
|
||||
box.textContent = 'Nuova password per ' + data.username + ': ' + data.new_password;
|
||||
box.classList.remove('hidden');
|
||||
toast('Password reimpostata.', 'success');
|
||||
} catch (e) {
|
||||
toast('Errore: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRounds() {
|
||||
try {
|
||||
const rounds = await callAdmin('GET', '/admin/rounds');
|
||||
@@ -662,11 +686,29 @@ document.getElementById('admin-token').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') adminLogin();
|
||||
});
|
||||
|
||||
if (adminToken) {
|
||||
function initAuthState() {
|
||||
adminToken = sessionStorage.getItem('plm_admin_token');
|
||||
if (!adminToken) {
|
||||
document.getElementById('dashboard-section').classList.add('hidden');
|
||||
document.getElementById('login-section').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
callAdmin('GET', '/admin/config')
|
||||
.then(() => { showDashboard(); return loadDashboard(); })
|
||||
.catch(() => { sessionStorage.removeItem('plm_admin_token'); adminToken = null; });
|
||||
.catch(() => adminLogout());
|
||||
}
|
||||
|
||||
// Bfcache can restore a frozen snapshot of this page (DOM/JS state as it was
|
||||
// before navigating away) without re-running any of this script — so a stale
|
||||
// view could survive across back/forward navigation, e.g. showing a dashboard
|
||||
// for a token that's since been rotated or explicitly logged out of. Cache-
|
||||
// Control: no-store on this response should already prevent that, but
|
||||
// re-validate here too as a safety net for browsers that ignore it.
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
if (event.persisted) initAuthState();
|
||||
});
|
||||
|
||||
initAuthState();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user