from datetime import datetime, timedelta, timezone import bcrypt import jwt from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.models.user import User def hash_password(plain: str) -> str: return bcrypt.hashpw(plain.encode(), bcrypt.gensalt()).decode() def verify_password(plain: str, hashed: str) -> bool: return bcrypt.checkpw(plain.encode(), hashed.encode()) def create_access_token(user_id: str, remember_me: bool = False) -> str: minutes = settings.JWT_EXPIRE_REMEMBER_MINUTES if remember_me else settings.JWT_EXPIRE_MINUTES expire = datetime.now(timezone.utc) + timedelta(minutes=minutes) payload = {"sub": user_id, "exp": expire} return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) def decode_access_token(token: str) -> str | None: """Decode JWT and return user_id, or None if invalid.""" try: payload = jwt.decode( token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM] ) return payload.get("sub") except jwt.PyJWTError: return None async def authenticate_user( db: AsyncSession, username: str, password: str ) -> User | None: result = await db.execute(select(User).where(User.username == username)) user = result.scalar_one_or_none() if not user: # Try phone number lookup for frontend users result = await db.execute(select(User).where(User.phone == username)) user = result.scalar_one_or_none() if not user or not verify_password(password, user.hashed_password): return None if not user.is_active: return None return user