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:
@@ -94,6 +94,14 @@ CREATE TABLE IF NOT EXISTS stock_log (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS stock_log_part ON stock_log(part_id, id DESC);
|
||||
|
||||
-- Small key/value store for things that must outlive a container rebuild and
|
||||
-- be changeable without editing a file on the host: the password hash and the
|
||||
-- session epoch live here, in the /data volume.
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS parts_fts USING fts5(
|
||||
name, description, manufacturer, mpn, specs, tags, category, location,
|
||||
tokenize='unicode61 remove_diacritics 2'
|
||||
@@ -131,6 +139,19 @@ def init():
|
||||
_initialised = True
|
||||
|
||||
|
||||
def get_setting(conn, key: str, default: str | None = None) -> str | None:
|
||||
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
|
||||
return row["value"] if row else default
|
||||
|
||||
|
||||
def set_setting(conn, key: str, value: str):
|
||||
conn.execute(
|
||||
"INSERT INTO settings(key, value) VALUES (?, ?) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
(key, str(value)),
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session():
|
||||
conn = connect()
|
||||
|
||||
Reference in New Issue
Block a user