Download auth.py from InsightUX/Insight_UX_1.0: direct link, hf CLI and curl.
- Browser
- Download file 5.08 kB
-
https://huggingface.co/InsightUX/Insight_UX_1.0/resolve/main/auth.py
- Command line
-
hf download hf://InsightUX/Insight_UX_1.0/auth.py
-
curl -L -o auth.py https://huggingface.co/InsightUX/Insight_UX_1.0/resolve/main/auth.py
5.08 kB
| """ | |
| auth.py | |
| Local, file-based multi-profile authentication for InsightUX. | |
| InsightUX is a single-process desktop app with no server and no database — | |
| this module IS the whole "backend": a JSON file of profiles (users.json, | |
| one entry per person who has ever used this install) plus salted-hash | |
| password verification. Plaintext passwords are never stored, logged, or | |
| returned to the caller. | |
| No heavy imports (stdlib only), so this can be imported cheaply from | |
| browser_session.py, calibrate.py, and validate.py alike — same reasoning | |
| theme.py already documents for its own "no cv2/mediapipe" import. | |
| """ | |
| import os | |
| import json | |
| import uuid | |
| import hmac | |
| import hashlib | |
| import secrets | |
| from datetime import datetime, timezone | |
| PBKDF2_ITERATIONS = 200_000 | |
| _HASH_NAME = "sha256" | |
| class AuthError(Exception): | |
| """Raised for user-facing auth failures (bad password, duplicate | |
| email, missing fields, ...) — callers show str(e) directly as the | |
| status message, same pattern as the rest of the app's _set_status calls.""" | |
| def _hash_password(password, salt=None): | |
| """Returns (salt_hex, hash_hex). A fresh random salt is generated | |
| unless one is supplied (re-hashing a login attempt for comparison).""" | |
| if salt is None: | |
| salt = secrets.token_bytes(16) | |
| elif isinstance(salt, str): | |
| salt = bytes.fromhex(salt) | |
| digest = hashlib.pbkdf2_hmac(_HASH_NAME, password.encode("utf-8"), salt, PBKDF2_ITERATIONS) | |
| return salt.hex(), digest.hex() | |
| def verify_password(password, salt_hex, hash_hex): | |
| _, candidate_hex = _hash_password(password, salt_hex) | |
| return hmac.compare_digest(candidate_hex, hash_hex) | |
| def _users_path(data_dir): | |
| return os.path.join(data_dir, "users.json") | |
| def load_users(data_dir): | |
| """{user_id: {...}} — empty dict if the file doesn't exist yet (first | |
| run) or is unreadable, never raises.""" | |
| path = _users_path(data_dir) | |
| if not os.path.exists(path): | |
| return {} | |
| try: | |
| with open(path, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except (json.JSONDecodeError, OSError): | |
| return {} | |
| def save_users(data_dir, users): | |
| os.makedirs(data_dir, exist_ok=True) | |
| path = _users_path(data_dir) | |
| tmp = path + ".tmp" | |
| with open(tmp, "w", encoding="utf-8") as f: | |
| json.dump(users, f, indent=2) | |
| os.replace(tmp, path) # atomic on POSIX and Windows -- never leaves users.json half-written | |
| def _avatar_initial(name): | |
| name = (name or "").strip() | |
| return (name[0] if name else "?").upper() | |
| def public_profile(record): | |
| """Strip salt/hash before this ever reaches JS -- every function that | |
| hands profile data back to the frontend routes through this.""" | |
| return { | |
| "id": record["id"], | |
| "name": record["name"], | |
| "email": record["email"], | |
| "avatar": record.get("avatar") or _avatar_initial(record.get("name")), | |
| "created_at": record.get("created_at"), | |
| } | |
| def create_user(data_dir, name, email, password): | |
| name = (name or "").strip() | |
| email = (email or "").strip().lower() | |
| if not name: | |
| raise AuthError("Name is required.") | |
| if not email or "@" not in email: | |
| raise AuthError("A valid email is required.") | |
| if not password or len(password) < 6: | |
| raise AuthError("Password must be at least 6 characters.") | |
| users = load_users(data_dir) | |
| for existing in users.values(): | |
| if existing.get("email", "").lower() == email: | |
| raise AuthError("A profile with that email already exists.") | |
| user_id = uuid.uuid4().hex[:12] | |
| salt_hex, hash_hex = _hash_password(password) | |
| record = { | |
| "id": user_id, | |
| "name": name, | |
| "email": email, | |
| "salt": salt_hex, | |
| "hash": hash_hex, | |
| "avatar": _avatar_initial(name), | |
| "created_at": datetime.now(timezone.utc).isoformat(), | |
| } | |
| users[user_id] = record | |
| save_users(data_dir, users) | |
| return public_profile(record) | |
| def verify_login(data_dir, user_id, password): | |
| """Returns the public profile dict on success, raises AuthError | |
| (never a KeyError/generic exception) on any failure — bad id, missing | |
| password, wrong password all look the same to the caller.""" | |
| users = load_users(data_dir) | |
| record = users.get(user_id) | |
| if not record or not password or not verify_password(password, record["salt"], record["hash"]): | |
| raise AuthError("Incorrect email/profile or password.") | |
| return public_profile(record) | |
| def list_profiles(data_dir): | |
| """Public profile list for the login/switch-profile picker -- never | |
| includes salt/hash. Sorted by name for a stable, predictable picker.""" | |
| users = load_users(data_dir) | |
| profiles = [public_profile(u) for u in users.values()] | |
| profiles.sort(key=lambda p: p["name"].lower()) | |
| return profiles | |
| def user_dir(data_dir, user_id): | |
| """Every per-user file (sessions/, calibration.pkl, the fine-tuned | |
| ONNX) lives under this one root — the single place that maps a user | |
| id to a filesystem path.""" | |
| return os.path.join(data_dir, "users", user_id) | |