From a957cc59f8f5ed676cb3c20ccd95abf3f7239582 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 21:29:02 +0200 Subject: [PATCH] Add /api/amber/set-pin: hidden pinHash rejects client writes (#13/#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testing verify-pin against the live DB showed the correct PIN failing: the server never held a hash at all. PocketBase silently drops client PATCHes to hidden fields, so the app's best-effort `PATCH {pinHash}` had never landed — the device-local verifier masked it on the PC. The new owner-scoped route writes the hash from superuser context (which can set hidden fields); pin "" clears it. Hash shape matches the app and verify-pin: sha256(":") hex. Co-Authored-By: Claude Opus 4.8 --- pb_hooks/profile_pin.pb.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pb_hooks/profile_pin.pb.js b/pb_hooks/profile_pin.pb.js index bd1cf2a..1148c4c 100644 --- a/pb_hooks/profile_pin.pb.js +++ b/pb_hooks/profile_pin.pb.js @@ -67,3 +67,35 @@ routerAdd("POST", "/api/amber/verify-pin", (e) => { } return e.json(200, { ok: ok }) }, $apis.requireAuth("users")) + +// ── POST /api/amber/set-pin ─────────────────────────────────────────────────── +// Auth: account owner. Body: { profileId, pin } — pin "" clears. Exists because +// `pinHash` is hidden and PocketBase **silently drops client writes to hidden +// fields**: the app's old best-effort `PATCH {pinHash}` never landed, so the +// server never held a hash and verify-pin had nothing to compare (the PC's +// local verifier masked this). Hooks run in superuser context, which can write +// hidden fields; the hash shape matches the app + verify-pin: +// sha256(":") hex. +routerAdd("POST", "/api/amber/set-pin", (e) => { + const data = new DynamicModel({ profileId: "", pin: "" }) + e.bindBody(data) + const profileId = (data.profileId || "").trim() + const pin = (data.pin || "").trim() + if (profileId === "" || (pin !== "" && !/^\d{4}$/.test(pin))) { + return e.json(400, { ok: false, error: "profileId and a 4-digit (or empty) pin are required" }) + } + + let rec = null + try { + rec = $app.findRecordById("profiles", profileId) + } catch (_) { + return e.json(404, { ok: false, error: "profile not found" }) + } + if (rec.getString("user") !== e.auth.id) { + return e.json(404, { ok: false, error: "profile not found" }) + } + + rec.set("pinHash", pin === "" ? "" : $security.sha256(profileId + ":" + pin)) + $app.save(rec) + return e.json(200, { ok: true }) +}, $apis.requireAuth("users"))