Set a password from the dashboard

Asked for because a family member could not get a reset email. The cause turned
out not to be mail at all — PocketBase looks accounts up case-sensitively, he
was typing a capitalised address, and the endpoint answers 204 either way so
that nobody can use it to discover which addresses exist. The app now
lower-cases addresses, which should stop it recurring.

This stays for the case that fixes: somebody who has genuinely forgotten a
password and cannot receive mail. One field and one button per row of a table
only a superuser can load, so it grants no capability the page did not already
have — the same token already creates accounts.

The field is cleared the moment it succeeds. It exists to be typed and read
aloud once, and a password left sitting in an input is one that ends up in a
screenshot.

Verified against the live backend on a throwaway account: created it, signed in
with the first password, set a second from this path, signed in with the second,
was refused the first, deleted it.
This commit is contained in:
Claude 2026-09-05 18:34:57 +02:00
parent ad38989f00
commit 5b69b9ec6a

View file

@ -64,6 +64,8 @@
pre{margin:8px 0 0;background:#0b0c10;border:1px solid var(--line);border-radius:8px;
padding:10px;max-height:340px;overflow:auto;font-family:var(--mono);font-size:11.5px;
white-space:pre-wrap;word-break:break-word}
.accts input.pw{width:190px;margin-right:6px}
.accts td:last-child{white-space:nowrap}
.login{max-width:340px;margin:14vh auto;padding:0 16px}
.login section{padding:18px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
@ -675,11 +677,60 @@ async function setNoticeActive(id, to){
} catch (e) { alert("nepovedlo se: " + e.message); }
}
/**
* Set someone's password directly.
*
* Exists because the emailed reset cannot be relied on, and the reason is not
* the mail server: PocketBase looks accounts up **case-sensitively**, so a
* family member typing `Vojta.Markup@seznam.cz` for an account stored in
* lowercase matches nothing — and the endpoint answers 204 either way, so that
* nobody can use it to discover which addresses exist. From the sofa that is
* indistinguishable from mail being broken, which is how it was reported.
*
* The app now lower-cases addresses before sending them, so this should stop
* happening. This stays for the case it cannot fix: somebody who has genuinely
* forgotten a password and cannot receive mail.
*
* No confirmation prompt. The button is one row of a table only a superuser can
* load, the field says what it does, and an accidental press with an empty box
* does nothing.
*/
async function setPassword(id, btn){
var input = el("pw_" + id);
var out = el("pwout_" + id);
var pw = (input.value || "").trim();
out.style.color = "";
if (pw.length < 8) { out.textContent = "nejméně 8 znaků"; return; }
btn.disabled = true;
out.textContent = "nastavuji…";
try {
// A superuser may set a password without the old one. `passwordConfirm` is
// required by the collection rules even so.
await api("/api/collections/users/records/" + id,
{ method: "PATCH", body: { password: pw, passwordConfirm: pw } });
// Cleared immediately: it was only ever here to be typed and handed over,
// and a password left sitting in a field is one that ends up in a
// screenshot.
input.value = "";
out.textContent = "hotovo — předej mu ho";
} catch (e) {
out.style.color = "var(--bad)";
out.textContent = "nepovedlo se: " + e.message;
} finally {
btn.disabled = false;
}
}
function viewAdmin(users, tpls){
var rows = (users || []).map(function(u){
var id = esc(u.id);
return "<tr><td>" + esc(u.email || u.username || u.id) + "</td>" +
"<td class='mono'>" + esc(u.ratingDefault || "—") + "</td>" +
"<td class='mono'>" + esc(String(u.created).substring(0, 10)) + "</td></tr>";
"<td class='mono'>" + esc(String(u.created).substring(0, 10)) + "</td>" +
"<td><input class='pw' id='pw_" + id + "' type='text' autocomplete='off' " +
"placeholder='nové heslo, nejméně 8 znaků'>" +
"<button data-pw='" + id + "'>Nastavit</button>" +
"<span class='note' id='pwout_" + id + "'></span></td></tr>";
}).join("");
return '<section class="wide"><h2>Nový účet</h2>' +
@ -711,8 +762,12 @@ function viewAdmin(users, tpls){
"tohle jde přes superuživatele, kterým jsi právě přihlášený.</p></section>" +
'<section class="wide"><h2>Účty</h2><table class="accts">' +
"<tr><th>e-mail</th><th>strop</th><th>vznik</th></tr>" +
(rows || "<tr><td colspan=5 class='note'>nic</td></tr>") + "</table>" +
"<tr><th>e-mail</th><th>strop</th><th>vznik</th><th>heslo</th></tr>" +
(rows || "<tr><td colspan=4 class='note'>nic</td></tr>") + "</table>" +
'<p class="note">Heslo nastavíš rovnou, bez e-mailu — reset přes e-mail ' +
"selhává tiše, když si adresu někdo napíše s velkým písmenem, protože " +
"PocketBase hledá účty přesně na znak a stejně vždycky odpoví „odesláno“, " +
"aby přes něj nešlo zjišťovat, které adresy existují.</p>" +
'<p class="note">Varianta rozhoduje, jaký build dostane přes automatickou ' +
"aktualizaci — server ji vybírá podle tohohle příznaku, klient si ji nemůže " +
"vyžádat.</p></section>";
@ -1019,6 +1074,9 @@ function wire(){
b.onclick = function(){ showLog(b.getAttribute("data-log"), b); };
});
if (el("createBtn")) el("createBtn").onclick = createAccount;
Array.prototype.forEach.call(el("main").querySelectorAll("button[data-pw]"), function(b){
b.onclick = function(){ setPassword(b.getAttribute("data-pw"), b); };
});
if (el("nSave")) el("nSave").onclick = saveNotice;
Array.prototype.forEach.call(el("main").querySelectorAll("button[data-notice]"), function(b){
b.onclick = function(){ setNoticeActive(b.getAttribute("data-notice"), b.getAttribute("data-to")); };