Manage the password in the app; close remaining audit findings

Password management moves out of the CLI entirely. The credential is now a
salted scrypt hash in the database (so it survives rebuilds, living in the /data
volume) rather than an environment variable; PARTS_PASSWORD is demoted to a
bootstrap value that stops working the moment a password is set in the UI. Every
token carries a session epoch, so changing the password — or "sign out other
devices" — invalidates outstanding cookies while keeping the browser that made
the change signed in. A banner nags until the handed-over password is replaced.
app/admin.py remains for the one case the UI cannot cover, a forgotten password.

Audit findings:
- Taxonomy update and delete scanned affected parts before taking the write
  lock, so a concurrent rename could leave the search index matching a name the
  UI no longer showed. All four routes now lock first; removing the lock again
  makes the new test fail exactly that way.
- History of a missing part returned 200 with an empty list; now 404.
- Infinity and NaN passed ge=0 and failed at the database. They are rejected as
  422 now, and the validation error handler no longer chokes trying to echo a
  non-finite value back.

Checks go from 134 to 181.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jay
2026-08-24 10:31:06 -04:00
parent 7bdf276342
commit ab82b5e9a9
11 changed files with 626 additions and 49 deletions
+43
View File
@@ -107,6 +107,49 @@ def main():
check("PATCH log's final quantity_after matches stored",
h3[0]["quantity_after"] == stored, f"log={h3[0]['quantity_after']} stored={stored}")
# --- taxonomy renames must leave the index agreeing with the tree ---
# The scan for affected parts and the reindex that follows have to see
# one consistent tree. Without the write lock a concurrent rename slips
# between them, and search keeps matching a name the UI no longer shows.
# Names are chosen so no token is a prefix of another: search uses prefix
# matching, so "Taxo 1" would legitimately match "Taxo 19" and the test
# would report a race that isn't there.
WORDS = ["alfa", "bravo", "charlie", "delta", "echo", "foxtrot", "golf",
"hotel", "india", "juliett", "kilo", "lima", "mike", "november",
"oscar", "papa", "quebec", "romeo", "sierra", "tango"]
cat = req("/api/categories", "POST", {"name": "Taxo zulu"})["id"]
req("/api/parts", "POST", {"name": "taxo widget", "category_id": cat, "quantity": 1})
names = [f"Taxo {w}" for w in WORDS]
with ThreadPoolExecutor(max_workers=20) as ex:
list(ex.map(lambda n: req(f"/api/categories/{cat}", "PATCH", {"name": n}), names))
final = [c["name"] for c in req("/api/categories")["items"] if c["id"] == cat][0]
hits_final = req(f"/api/parts?q={final.replace(' ', '+')}")["total"]
check("search matches the category's final name", hits_final == 1, f"{final} -> {hits_final}")
stale = [n for n in ["Taxo zulu"] + names if n != final
and req(f"/api/parts?q={n.replace(' ', '+')}")["total"] > 0]
check("no superseded category name still matches", not stale, f"stale: {stale}")
# Same race with a location, and with parts being created concurrently.
loc = req("/api/locations", "POST", {"name": "Loc zulu"})["id"]
loc_names = [f"Loc {w}" for w in WORDS[:15]]
def rename_or_add(i):
if i % 3 == 0:
req("/api/parts", "POST", {"name": f"loc widget {i}", "location_id": loc, "quantity": 1})
else:
req(f"/api/locations/{loc}", "PATCH", {"name": loc_names[i % len(loc_names)]})
with ThreadPoolExecutor(max_workers=15) as ex:
list(ex.map(rename_or_add, range(15)))
final_loc = [l["name"] for l in req("/api/locations")["items"] if l["id"] == loc][0]
in_loc = req(f"/api/parts?location_id={loc}&limit=100")["total"]
by_name = req(f"/api/parts?q={final_loc.replace(' ', '+')}&limit=100")["total"]
check("every part in the location is indexed under its final name",
by_name == in_loc, f"filter={in_loc} search={by_name}")
stale_loc = [n for n in ["Loc zulu"] + loc_names if n != final_loc
and req(f"/api/parts?q={n.replace(' ', '+')}&limit=100")["total"] > 0]
check("no superseded location name still matches", not stale_loc, f"stale: {stale_loc}")
# --- concurrent creates don't collide ---
with ThreadPoolExecutor(max_workers=25) as ex:
ids = list(ex.map(