Parts inventory: catalog, search, and stock tracking

FastAPI + SQLite behind a single-page frontend, deployed as a container on
mainserver behind Caddy.

One flexible parts table plus key/value specs so components, filament,
fasteners and tooling share a schema. Categories and locations are nestable
trees whose filters and sidebar counts both roll up through descendants.
Category spec templates pre-fill the properties worth recording for each kind
of part, which is what makes manual entry tolerable. Every quantity change is
logged. Search is FTS5 with prefix matching across names, MPNs, spec values,
tags, category and location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jay
2026-08-24 09:19:58 -04:00
commit ffe508b7f3
15 changed files with 2330 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
"""Single-user login gate.
The app sits on a public hostname, so it needs a door. One shared password from
the environment buys a signed, expiring cookie — no user table, no password
reset flow, nothing to administer. Set PARTS_AUTH=off for LAN-only use.
"""
import base64
import hashlib
import hmac
import os
import time
from fastapi import Cookie, HTTPException
COOKIE_NAME = "parts_session"
def _env(name: str, default: str = "") -> str:
return os.environ.get(name, default).strip()
def auth_enabled() -> bool:
return _env("PARTS_AUTH", "on").lower() not in ("off", "0", "false", "no")
def _secret() -> bytes:
secret = _env("PARTS_SECRET")
if not secret:
# Without a configured secret, derive one from the password so sessions
# are still unforgeable — they just don't survive a password change.
secret = "fallback:" + _env("PARTS_PASSWORD")
return hashlib.sha256(secret.encode()).digest()
def session_days() -> int:
try:
return max(1, int(_env("PARTS_SESSION_DAYS", "30")))
except ValueError:
return 30
def check_password(candidate: str) -> bool:
expected = _env("PARTS_PASSWORD")
if not expected:
return False
return hmac.compare_digest(candidate.encode(), expected.encode())
def issue_token() -> str:
expires = int(time.time()) + session_days() * 86400
payload = str(expires).encode()
sig = hmac.new(_secret(), payload, hashlib.sha256).digest()
return base64.urlsafe_b64encode(payload + b"." + sig).decode()
def token_valid(token: str) -> bool:
try:
raw = base64.urlsafe_b64decode(token.encode())
payload, sig = raw.rsplit(b".", 1)
expected = hmac.new(_secret(), payload, hashlib.sha256).digest()
if not hmac.compare_digest(sig, expected):
return False
return int(payload) > time.time()
except Exception:
return False
def require_auth(parts_session: str | None = Cookie(default=None)):
"""FastAPI dependency guarding every data route."""
if not auth_enabled():
return True
if parts_session and token_valid(parts_session):
return True
raise HTTPException(status_code=401, detail="Not authenticated")