From fb59f504fe20cfaf7c40193ac73935d5f9602f01 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 19:04:50 +0200 Subject: [PATCH] Czech account emails, and a reset that finishes on our own domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reset email works — confirmed by a delivered message, after every attempt on record had failed at the lookup and the mail path had never once been exercised. What the delivered message showed was a different problem: it was in English, and its button pointed at `pb.petruzalekr.cz/_/#/auth/confirm-password-reset/…`, which is PocketBase's admin console. Every other surface of this app is Czech on purpose, because the people using it are the owner's parents and his children — and the one moment they are most likely to be stuck is the one moment it started speaking English and sent them to what reads like a developer tool. Found by looking at a delivered email rather than at the settings. The templates were simply PocketBase's defaults, and a default is invisible until somebody receives one. The reset link now lands on amber.petruzalekr.cz, which hosts the form: two fields, in Czech, on the domain people already know. A reset link wins over a stored session, because somebody arriving with one is trying to fix their account and dropping them into a signed-in page hides the thing they came to do. The token is stripped from the address bar once spent — a URL carrying a credential is one that ends up in a bookmark or a screenshot. A failed confirm names the likely cause. These links expire, and "something went wrong" gives somebody who opened yesterday's email no way to know that. Verification and email-change keep PocketBase's own confirm pages: nobody has used those, they are not part of the report, and pointing them at a page that does not exist would be worse than English. As a migration rather than a click in the admin UI, so restoring the backend from migrations does not quietly put English back. --- .../1794000000_czech_mail_templates.js | 83 +++++++++++++++++++ pb_public/index.html | 61 ++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 pb_migrations/1794000000_czech_mail_templates.js diff --git a/pb_migrations/1794000000_czech_mail_templates.js b/pb_migrations/1794000000_czech_mail_templates.js new file mode 100644 index 0000000..dd98ac7 --- /dev/null +++ b/pb_migrations/1794000000_czech_mail_templates.js @@ -0,0 +1,83 @@ +/// + +// The account emails were in English, and pointed at PocketBase's admin console. +// +// **Why it matters here.** Every other surface of this app is Czech, on purpose: +// the people using it are the owner's parents and his children. The one moment +// they are most likely to be stuck — locked out, asking for a new password — was +// the one moment the product started speaking English and sent them to +// `pb.petruzalekr.cz/_/#/auth/confirm-password-reset/…`, which is the admin UI's +// own page and reads to anybody's parent like a developer tool opened by +// mistake. +// +// Found by looking at a delivered message rather than at the settings: the +// templates were simply PocketBase's defaults, and defaults are invisible until +// somebody receives one. +// +// The links now point at amber.petruzalekr.cz, which hosts the confirm form in +// Czech (see pb_public/index.html, `?reset=`). Verification and email-change +// keep PocketBase's own confirm pages: nobody has hit those yet, they are not +// part of the reported problem, and pointing them somewhere that does not exist +// would be worse than English. +const RESET_SUBJECT = "Nové heslo k Amberu"; +const RESET_BODY = `

Ahoj,

+

Někdo (snad ty) požádal o nové heslo k Amberu. Nastavíš si ho tímhle odkazem:

+

+ Nastavit nové heslo +

+

Pokud jsi o nic nežádal, tenhle e-mail klidně smaž. Nic se nestalo a heslo +zůstává, jaké bylo.

+

Amber

`; + +const VERIFY_SUBJECT = "Ověření e-mailu pro Amber"; +const VERIFY_BODY = `

Ahoj,

+

Potvrď prosím tímhle odkazem, že tenhle e-mail patří tobě:

+

+ Potvrdit e-mail +

+

Amber

`; + +const CHANGE_SUBJECT = "Změna e-mailu u Amberu"; +const CHANGE_BODY = `

Ahoj,

+

Potvrď prosím změnu e-mailu u svého účtu:

+

+ Potvrdit změnu +

+

Pokud jsi o změnu nežádal, ozvi se Richardovi — někdo se ti hrabe v účtu.

+

Amber

`; + +// The defaults, so the down migration restores exactly what was there. +const EN = { + reset: { + subject: "Reset your {APP_NAME} password", + body: `

Hello,

+

Click on the button below to reset your password.

+

+ Reset password +

+

If you didn't ask to reset your password, please ignore this email.

+

+ Thanks,
+ {APP_NAME} team +

`, + }, +}; + +migrate( + (app) => { + const users = app.findCollectionByNameOrId("users"); + users.resetPasswordTemplate.subject = RESET_SUBJECT; + users.resetPasswordTemplate.body = RESET_BODY; + users.verificationTemplate.subject = VERIFY_SUBJECT; + users.verificationTemplate.body = VERIFY_BODY; + users.confirmEmailChangeTemplate.subject = CHANGE_SUBJECT; + users.confirmEmailChangeTemplate.body = CHANGE_BODY; + app.save(users); + }, + (app) => { + const users = app.findCollectionByNameOrId("users"); + users.resetPasswordTemplate.subject = EN.reset.subject; + users.resetPasswordTemplate.body = EN.reset.body; + app.save(users); + }, +); diff --git a/pb_public/index.html b/pb_public/index.html index 1e30eea..efd4070 100644 --- a/pb_public/index.html +++ b/pb_public/index.html @@ -106,6 +106,25 @@
+ + + +
@@ -1354,10 +1373,52 @@ $("acChange").onclick=async function(){ await loadAddons(); }; +// ── setting a new password from an emailed link ────────────────────────────── + +/** The token the reset email put in the address bar, or null. */ +function resetToken(){ + try { return new URLSearchParams(location.search).get("reset"); } + catch(_){ return null; } +} + +function showReset(){ + $("anon").hidden = true; + $("reset").hidden = false; + $("rNew").focus(); +} + +$("rSave").onclick=async function(){ + var tok=resetToken(); + var a=$("rNew").value, b=$("rNew2").value; + if(a.length<8){ setMsg($("rMsg"),"Heslo musí mít aspoň 8 znaků.","err"); return; } + if(a!==b){ setMsg($("rMsg"),"Hesla se neshodují.","err"); return; } + setMsg($("rMsg"),""); busy($("rSave"),true,"Nastavuji…"); + var r=await api("POST","/api/collections/users/confirm-password-reset", + { token:tok, password:a, passwordConfirm:b }, {anon:true}); + busy($("rSave"),false); + if(!r.ok){ + // The overwhelmingly likely cause, and the one worth naming: these links + // expire, and a person who tried yesterday's email has no way to know that + // from "something went wrong". + setMsg($("rMsg"),"Nepovedlo se. Odkaz nejspíš vypršel — požádej o nový.","err"); + return; + } + $("rNew").value=$("rNew2").value=""; + setMsg($("rMsg"),"Hotovo. Přihlas se novým heslem.","ok"); + // Drop the token from the address bar: it is spent, and a URL with a + // credential in it is one that ends up in a bookmark or a shared screenshot. + try { history.replaceState(null,"",location.pathname); } catch(_){} + setTimeout(function(){ $("reset").hidden=true; $("anon").hidden=false; }, 2500); +}; + // ── boot ───────────────────────────────────────────────────────────────────── (async function(){ fillLangs(); $("tvUrl").textContent=location.host; + // A reset link wins over a stored session: somebody arriving with one is + // trying to fix their account, and dropping them into a signed-in page they + // did not ask for hides the very thing they came to do. + if (resetToken()) { showReset(); return; } if (await restore()) await enter(); })();