""" Simple username + password authentication for DiffLens. User credentials are stored in users.json next to this file: {"users": {"alice": "$2b$12$..."}} The hash is bcrypt. Sessions use Flask's signed cookie (SECRET_KEY must be set in the environment for production). """ import json import os import secrets from functools import wraps import bcrypt from flask import session, jsonify, request USERS_FILE = os.path.join(os.path.dirname(__file__), "users.json") def _load_users(): if not os.path.exists(USERS_FILE): return {} with open(USERS_FILE, "r") as f: data = json.load(f) or {} return data.get("users", {}) def _save_users(users): with open(USERS_FILE, "w") as f: json.dump({"users": users}, f, indent=2) def hash_password(plain): return bcrypt.hashpw(plain.encode("utf-8"), bcrypt.gensalt(12)).decode("ascii") def verify_password(plain, hashed): try: return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8")) except Exception: return False def create_user(username, password): users = _load_users() users[username] = hash_password(password) _save_users(users) def authenticate(username, password): users = _load_users() h = users.get(username) if not h: return False return verify_password(password, h) def get_secret_key(): """Returns the Flask SECRET_KEY. Reads from env, falls back to a file that's auto-generated on first run so the key persists across restarts.""" key = os.environ.get("DIFFLENS_SECRET_KEY") if key: return key path = os.path.join(os.path.dirname(__file__), ".secret_key") if os.path.exists(path): with open(path, "r") as f: return f.read().strip() key = secrets.token_urlsafe(48) with open(path, "w") as f: f.write(key) os.chmod(path, 0o600) return key def login_required(view): """Decorator that returns 401 for unauthenticated requests.""" @wraps(view) def wrapper(*args, **kwargs): if not session.get("user"): return jsonify({"error": "unauthorized"}), 401 return view(*args, **kwargs) return wrapper def current_user(): return session.get("user")