Czech account emails, and a reset that finishes on our own domain
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.
This commit is contained in:
parent
2b458fb9f6
commit
fb59f504fe
2 changed files with 144 additions and 0 deletions
83
pb_migrations/1794000000_czech_mail_templates.js
Normal file
83
pb_migrations/1794000000_czech_mail_templates.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// 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 = `<p>Ahoj,</p>
|
||||
<p>Někdo (snad ty) požádal o nové heslo k Amberu. Nastavíš si ho tímhle odkazem:</p>
|
||||
<p>
|
||||
<a class="btn" href="https://amber.petruzalekr.cz/?reset={TOKEN}" target="_blank" rel="noopener">Nastavit nové heslo</a>
|
||||
</p>
|
||||
<p><i>Pokud jsi o nic nežádal, tenhle e-mail klidně smaž. Nic se nestalo a heslo
|
||||
zůstává, jaké bylo.</i></p>
|
||||
<p>Amber</p>`;
|
||||
|
||||
const VERIFY_SUBJECT = "Ověření e-mailu pro Amber";
|
||||
const VERIFY_BODY = `<p>Ahoj,</p>
|
||||
<p>Potvrď prosím tímhle odkazem, že tenhle e-mail patří tobě:</p>
|
||||
<p>
|
||||
<a class="btn" href="{APP_URL}/_/#/auth/confirm-verification/{TOKEN}" target="_blank" rel="noopener">Potvrdit e-mail</a>
|
||||
</p>
|
||||
<p>Amber</p>`;
|
||||
|
||||
const CHANGE_SUBJECT = "Změna e-mailu u Amberu";
|
||||
const CHANGE_BODY = `<p>Ahoj,</p>
|
||||
<p>Potvrď prosím změnu e-mailu u svého účtu:</p>
|
||||
<p>
|
||||
<a class="btn" href="{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}" target="_blank" rel="noopener">Potvrdit změnu</a>
|
||||
</p>
|
||||
<p><i>Pokud jsi o změnu nežádal, ozvi se Richardovi — někdo se ti hrabe v účtu.</i></p>
|
||||
<p>Amber</p>`;
|
||||
|
||||
// The defaults, so the down migration restores exactly what was there.
|
||||
const EN = {
|
||||
reset: {
|
||||
subject: "Reset your {APP_NAME} password",
|
||||
body: `<p>Hello,</p>
|
||||
<p>Click on the button below to reset your password.</p>
|
||||
<p>
|
||||
<a class="btn" href="{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}" target="_blank" rel="noopener">Reset password</a>
|
||||
</p>
|
||||
<p><i>If you didn't ask to reset your password, please ignore this email.</i></p>
|
||||
<p>
|
||||
Thanks,<br/>
|
||||
{APP_NAME} team
|
||||
</p>`,
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
|
@ -106,6 +106,25 @@
|
|||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- ── setting a new password from an emailed link ─────────────────────── -->
|
||||
<!-- Hosted here rather than letting the email point at PocketBase's own
|
||||
confirm page: that page lives under /_/ with the admin console, is in
|
||||
English, and looks to anybody's parents like a developer tool they have
|
||||
opened by mistake. -->
|
||||
<div id="reset" hidden>
|
||||
<div class="card">
|
||||
<h1>Nové heslo</h1>
|
||||
<p class="sub">Zadej si nové heslo k účtu. Odkaz z e-mailu platí omezenou
|
||||
dobu — když nezabere, požádej o nový.</p>
|
||||
<label for="rNew">Nové heslo</label>
|
||||
<input id="rNew" type="password" autocomplete="new-password">
|
||||
<label for="rNew2">Nové heslo znovu</label>
|
||||
<input id="rNew2" type="password" autocomplete="new-password">
|
||||
<button id="rSave" class="btn primary">Nastavit heslo</button>
|
||||
<div id="rMsg" class="msg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── signed out ───────────────────────────────────────────────────────── -->
|
||||
<div id="anon">
|
||||
<div class="card">
|
||||
|
|
@ -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();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue