Let people ask for a new password on the website, and stop capitals breaking it

Two halves of the same report. A family member could not get in, four password
resets produced nothing, and the site offered no way to ask for one anyway.

**Capitals.** The web sign-in trimmed the address but did not lower-case it, and
PocketBase looks accounts up case-sensitively — so `Vojta.Markup@seznam.cz` for
an account stored in lowercase matched nothing and read as a wrong password.
Phones capitalise the first letter of a text field by themselves, so this is the
mistake people will actually make. The same fix went into the app.

**The reset itself.** A link under the sign-in button, hidden until asked for:
the overwhelmingly common visit is somebody who remembers their password, and a
reset field on the way in invites people to reach for it first.

The message it shows says what was *done*, never whether the address matched.
The endpoint deliberately answers the same either way so that nobody can use it
to discover which addresses are registered — and pretending otherwise is exactly
what made this so hard to read from outside: four attempts, four "sent", nothing
arriving, no way to tell that the lookup had simply found nothing. It also
points at the one thing that always works when mail does not, which is asking
for it to be set directly.

Whether delivery itself works is still unverified: every reset attempt on record
failed at the lookup, so the mail path has never once been exercised. A reset
has been triggered against a real account to settle that.
This commit is contained in:
Claude 2026-09-05 19:01:02 +02:00
parent 5b69b9ec6a
commit 2b458fb9f6

View file

@ -118,6 +118,18 @@
<input id="pass" type="password" autocomplete="current-password">
<button id="loginBtn" class="btn primary">Přihlásit se</button>
<div id="anonMsg" class="msg"></div>
<p class="hint"><a href="#" id="forgotLink">Zapomenuté heslo?</a></p>
<!-- Hidden until asked for: the overwhelmingly common visit is somebody
who remembers their password, and a reset field on the way in invites
people to reach for it first. -->
<div id="forgot" hidden>
<label for="fEmail">E-mail účtu</label>
<input id="fEmail" type="email" autocomplete="username" inputmode="email">
<button id="forgotBtn" class="btn">Poslat odkaz na nové heslo</button>
<div id="forgotMsg" class="msg"></div>
</div>
<p class="hint">Účty zakládá Richard — registrace tu není. Když se nemůžeš
dostat dovnitř, napiš mu.</p>
</div>
@ -505,7 +517,11 @@ async function restore(){
}
$("loginBtn").onclick=async function(){
var email=$("email").value.trim(), pass=$("pass").value;
// Lower-cased, not just trimmed. PocketBase looks accounts up
// case-sensitively, so an address typed with a capital — which phones do by
// themselves — matches nothing and reads as a wrong password. It cost a
// family member two days and four failed password resets.
var email=$("email").value.trim().toLowerCase(), pass=$("pass").value;
if(!email||!pass){ setMsg($("anonMsg"),"Vyplň e-mail i heslo.","err"); return; }
setMsg($("anonMsg"),""); busy($("loginBtn"),true,"Přihlašuji…");
var r=await api("POST","/api/collections/users/auth-with-password",
@ -516,6 +532,30 @@ $("loginBtn").onclick=async function(){
TOKEN=r.data.token; USER=r.data.record; PASSWORD=pass; saveSession();
await enter();
};
$("forgotLink").onclick=function(e){
e.preventDefault();
var box=$("forgot");
box.hidden=!box.hidden;
if(!box.hidden){ $("fEmail").value=$("email").value; $("fEmail").focus(); }
};
$("forgotBtn").onclick=async function(){
var email=$("fEmail").value.trim().toLowerCase();
if(!email){ setMsg($("forgotMsg"),"Vyplň e-mail.","err"); return; }
setMsg($("forgotMsg"),""); busy($("forgotBtn"),true,"Odesílám…");
var r=await api("POST","/api/collections/users/request-password-reset",
{ email:email }, {anon:true});
busy($("forgotBtn"),false);
// The server answers the same whether or not the account exists — on purpose,
// so nobody can use this to find out which addresses are registered. So the
// message says what was DONE, never whether it matched: promising "sent" for
// an address with no account is the lie that made the last outage so hard to
// read from the outside.
if(!r.ok){ setMsg($("forgotMsg"),"Nepovedlo se odeslat. Zkus to za chvíli.","err"); return; }
setMsg($("forgotMsg"),"Pokud k té adrese účet existuje, přišel na ni odkaz. "+
"Mrkni i do spamu. Když nic nedorazí, napiš Richardovi — heslo ti nastaví přímo.","ok");
};
$("logoutBtn").onclick=signOut;
// ── shell ────────────────────────────────────────────────────────────────────