from datetime import datetime, timedelta, timezone import logging import jwt from argon2 import PasswordHasher from argon2.exceptions import InvalidHashError, VerificationError from app.config import settings logger = logging.getLogger(__name__) # Shared by registration (app/auth/routes.py) and the self-service password change # (app/api/routes/users.py) so the two can't enforce different minimums. MIN_PASSWORD_LENGTH = 8 _hasher = PasswordHasher() def hash_password(password: str) -> str: return _hasher.hash(password) def verify_password(password: str, password_hash: str) -> bool: """Any failure to verify reads as "wrong password", never as a server error. Catching only VerifyMismatchError left the other two cases as unhandled 500s (B-13): VerificationError covers argon2's other verification failures, and InvalidHashError fires when the stored hash can't be parsed at all — which is a data problem worth logging, but from the caller's side it still just means this password does not open this account. """ try: return _hasher.verify(password_hash, password) except InvalidHashError: logger.error("stored password hash is unparseable — password verification cannot succeed") return False except VerificationError: return False def create_access_token(user_id: int) -> str: expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes) payload = {"sub": str(user_id), "exp": expires_at} return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) def decode_access_token(token: str) -> int: payload = jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) return int(payload["sub"])