ecom-qa / src /auth /auth.py
rnyx's picture
Initial deploy
f48b219
Raw
History Blame Contribute Delete
2.8 kB
"""
src/auth/auth.py
Fix #9 — User authentication with Flask-Login + bcrypt password hashing.
"""
import re
import logging
import bcrypt
from flask_login import UserMixin
logger = logging.getLogger(__name__)
USERNAME_RE = re.compile(r"^[a-zA-Z0-9_]{3,32}$")
class User(UserMixin):
"""Flask-Login compatible user model backed by SQLite."""
def __init__(self, id: int, username: str, email: str):
self.id = id
self.username = username
self.email = email
# ── Flask-Login required ──────────────────────────────────────────────────
def get_id(self) -> str:
return str(self.id)
# ── Factory methods ───────────────────────────────────────────────────────
@classmethod
def get(cls, user_id: int | str, db) -> "User | None":
row = db.get_user_by_id(int(user_id))
if row:
return cls(row["id"], row["username"], row["email"])
return None
@classmethod
def authenticate(cls, username: str, password: str, db) -> "User | None":
if not username or not password:
return None
row = db.get_user_by_username(username.strip())
if not row:
# Constant-time dummy check to prevent user-enumeration via timing
bcrypt.checkpw(b"dummy", bcrypt.hashpw(b"dummy", bcrypt.gensalt()))
return None
stored_hash = row["password_hash"]
if isinstance(stored_hash, str):
stored_hash = stored_hash.encode()
if bcrypt.checkpw(password.encode(), stored_hash):
return cls(row["id"], row["username"], row["email"])
return None
@classmethod
def create(cls, username: str, password: str, email: str, db) -> "User":
"""
Creates a new user. Raises ValueError on validation failure.
"""
username = (username or "").strip()
password = (password or "").strip()
email = (email or "").strip()
if not USERNAME_RE.match(username):
raise ValueError("Username must be 3–32 characters: letters, numbers, underscores only.")
if len(password) < 8:
raise ValueError("Password must be at least 8 characters.")
if db.get_user_by_username(username):
raise ValueError(f"Username '{username}' is already taken.")
pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
user_id = db.create_user(username, email, pw_hash)
logger.info("New user registered: %s (id=%d)", username, user_id)
return cls(user_id, username, email)