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:
parent
ad38989f00
commit
5b69b9ec6a
1 changed files with 61 additions and 3 deletions
|
|
@ -64,6 +64,8 @@
|
||||||
pre{margin:8px 0 0;background:#0b0c10;border:1px solid var(--line);border-radius:8px;
|
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;
|
padding:10px;max-height:340px;overflow:auto;font-family:var(--mono);font-size:11.5px;
|
||||||
white-space:pre-wrap;word-break:break-word}
|
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{max-width:340px;margin:14vh auto;padding:0 16px}
|
||||||
.login section{padding:18px}
|
.login section{padding:18px}
|
||||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
.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); }
|
} 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){
|
function viewAdmin(users, tpls){
|
||||||
var rows = (users || []).map(function(u){
|
var rows = (users || []).map(function(u){
|
||||||
|
var id = esc(u.id);
|
||||||
return "<tr><td>" + esc(u.email || u.username || u.id) + "</td>" +
|
return "<tr><td>" + esc(u.email || u.username || u.id) + "</td>" +
|
||||||
"<td class='mono'>" + esc(u.ratingDefault || "—") + "</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("");
|
}).join("");
|
||||||
|
|
||||||
return '<section class="wide"><h2>Nový účet</h2>' +
|
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>" +
|
"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">' +
|
'<section class="wide"><h2>Účty</h2><table class="accts">' +
|
||||||
"<tr><th>e-mail</th><th>strop</th><th>vznik</th></tr>" +
|
"<tr><th>e-mail</th><th>strop</th><th>vznik</th><th>heslo</th></tr>" +
|
||||||
(rows || "<tr><td colspan=5 class='note'>nic</td></tr>") + "</table>" +
|
(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 ' +
|
'<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 " +
|
"aktualizaci — server ji vybírá podle tohohle příznaku, klient si ji nemůže " +
|
||||||
"vyžádat.</p></section>";
|
"vyžádat.</p></section>";
|
||||||
|
|
@ -1019,6 +1074,9 @@ function wire(){
|
||||||
b.onclick = function(){ showLog(b.getAttribute("data-log"), b); };
|
b.onclick = function(){ showLog(b.getAttribute("data-log"), b); };
|
||||||
});
|
});
|
||||||
if (el("createBtn")) el("createBtn").onclick = createAccount;
|
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;
|
if (el("nSave")) el("nSave").onclick = saveNotice;
|
||||||
Array.prototype.forEach.call(el("main").querySelectorAll("button[data-notice]"), function(b){
|
Array.prototype.forEach.call(el("main").querySelectorAll("button[data-notice]"), function(b){
|
||||||
b.onclick = function(){ setNoticeActive(b.getAttribute("data-notice"), b.getAttribute("data-to")); };
|
b.onclick = function(){ setNoticeActive(b.getAttribute("data-notice"), b.getAttribute("data-to")); };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue