"""Auth service: password hashing, JWT, user queries.""" from datetime import datetime, timedelta, timezone from fastapi import HTTPException, status from jose import JWTError, ExpiredSignatureError, jwt from passlib.context import CryptContext from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.models.user import User, UserRole _pwd = CryptContext(schemes=["bcrypt"], deprecated="auto") def hash_password(password: str) -> str: return _pwd.hash(password) def verify_password(plain: str, hashed: str) -> bool: return _pwd.verify(plain, hashed) def create_access_token(user_id: str, role: str) -> str: expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes) payload = {"sub": user_id, "role": role, "exp": expire} return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) def decode_access_token(token: str) -> dict: try: return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) except ExpiredSignatureError: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired") except JWTError: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") async def get_user_by_id(db: AsyncSession, user_id: str) -> User | None: result = await db.execute(select(User).where(User.id == user_id)) return result.scalar_one_or_none() async def get_user_by_username(db: AsyncSession, username: str) -> User | None: result = await db.execute(select(User).where(User.username == username)) return result.scalar_one_or_none() async def authenticate_user(db: AsyncSession, username: str, password: str) -> User: user = await get_user_by_username(db, username) if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") # Check lock if user.locked_until and user.locked_until > datetime.utcnow(): raise HTTPException( status_code=status.HTTP_423_LOCKED, detail=f"Account locked until {user.locked_until.isoformat()}", ) if not verify_password(password, user.hashed_password): user.login_attempts += 1 if user.login_attempts >= 5: user.locked_until = datetime.utcnow() + timedelta(minutes=15) await db.commit() raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") # Success — reset user.login_attempts = 0 user.locked_until = None await db.commit() return user async def create_user(db: AsyncSession, username: str, email: str, password: str) -> User: result = await db.execute(select(User)) existing_count = len(result.scalars().all()) if existing_count == 0: role = UserRole.OWNER is_approved = True else: role = UserRole.VIEWER is_approved = False user = User( username=username, email=email, hashed_password=hash_password(password), role=role, is_approved=is_approved, ) db.add(user) await db.commit() await db.refresh(user) return user