import secrets from datetime import datetime, timedelta, timezone import bcrypt from jose import JWTError, jwt from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.models.user import User, UserSession ALGORITHM = "HS256" def hash_password(password: str) -> str: return bcrypt.hashpw(password.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: int) -> str: expire = datetime.now(timezone.utc) + timedelta(seconds=settings.ACCESS_TOKEN_EXPIRE_SECONDS) return jwt.encode({"sub": str(user_id), "exp": expire}, settings.SECRET_KEY, algorithm=ALGORITHM) def decode_access_token(token: str) -> int | None: try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) sub = payload.get("sub") if sub is None: return None return int(sub) except (JWTError, ValueError): 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.is_active == True)) user = result.scalar_one_or_none() if user is None or not verify_password(password, user.password_hash): return None return user async def create_session( db: AsyncSession, user_id: int, ip_address: str | None = None, user_agent: str | None = None ) -> str: token = secrets.token_urlsafe(32) now = datetime.now(timezone.utc) session = UserSession( user_id=user_id, refresh_token=token, expires_at=now + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS), created_at=now, ip_address=ip_address, user_agent=user_agent, ) db.add(session) await db.flush() return token async def refresh_session( db: AsyncSession, refresh_token: str, ip_address: str | None = None ) -> tuple[User, str] | None: now = datetime.now(timezone.utc) result = await db.execute( select(UserSession) .where(UserSession.refresh_token == refresh_token, UserSession.expires_at > now) ) session = result.scalar_one_or_none() if session is None: return None user_result = await db.execute(select(User).where(User.id == session.user_id, User.is_active == True)) user = user_result.scalar_one_or_none() if user is None: return None # Rotate: delete old session, create new await db.delete(session) new_token = await create_session(db, user.id, ip_address=ip_address) return user, new_token async def revoke_session(db: AsyncSession, refresh_token: str) -> None: await db.execute(delete(UserSession).where(UserSession.refresh_token == refresh_token)) async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None: result = await db.execute(select(User).where(User.id == user_id, User.is_active == True)) return result.scalar_one_or_none()