"""Authentication service: password hashing, login, token refresh, user lookup.""" import uuid from typing import Optional from passlib.context import CryptContext from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.user import User, UserRole from app.utils.jwt import ( create_access_token, create_refresh_token, verify_refresh_token, ) pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def hash_password(password: str) -> str: """Hash a plaintext password using bcrypt.""" return pwd_context.hash(password) def verify_password(plaintext: str, hashed: str) -> bool: """Verify a plaintext password against a bcrypt hash.""" return pwd_context.verify(plaintext, hashed) async def get_user_by_email(db: AsyncSession, email: str) -> Optional[User]: """Fetch a user by email address.""" result = await db.execute(select(User).where(User.email == email)) return result.scalar_one_or_none() async def get_user_by_id(db: AsyncSession, user_id: uuid.UUID) -> Optional[User]: """Fetch a user by UUID.""" result = await db.execute(select(User).where(User.id == user_id)) return result.scalar_one_or_none() async def authenticate_user(db: AsyncSession, email: str, password: str) -> Optional[User]: """Authenticate a user by email and password.""" user = await get_user_by_email(db, email) if user is None: return None if not user.is_active: return None if not verify_password(password, user.password_hash): return None return user def generate_token_pair(user: User) -> dict: """Create access + refresh JWT tokens for a given user.""" role_val = user.role.value if isinstance(user.role, UserRole) else str(user.role) access_token = create_access_token( user_id=str(user.id), role=role_val, email=user.email, lang=user.language, ) refresh_token = create_refresh_token( user_id=str(user.id), role=role_val, email=user.email, lang=user.language, ) from app.config import settings return { "access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer", "expires_in": settings.access_token_ttl_seconds, } async def refresh_access_token(db: AsyncSession, refresh_token: str) -> Optional[dict]: """Verify a refresh token and issue a new token pair.""" payload = verify_refresh_token(refresh_token) if payload is None: return None user_id_str = payload.get("sub") if not user_id_str: return None try: user_uuid = uuid.UUID(user_id_str) except (ValueError, TypeError): return None user = await get_user_by_id(db, user_uuid) if user is None or not user.is_active: return None return generate_token_pair(user) async def create_user( db: AsyncSession, email: str, password: str, full_name: str, role: str = "verkaeufer", language: str = "de", ) -> User: """Create a new user with a bcrypt-hashed password.""" existing = await get_user_by_email(db, email) if existing is not None: raise ValueError(f"User with email {email} already exists") user = User( email=email, password_hash=hash_password(password), full_name=full_name, role=UserRole(role), language=language, is_active=True, ) db.add(user) await db.flush() await db.refresh(user) return user async def list_users( db: AsyncSession, page: int = 1, page_size: int = 20, ) -> tuple[list[User], int]: """List users with pagination. Returns (users, total_count).""" count_result = await db.execute(select(func.count(User.id))) total = count_result.scalar() or 0 offset = (page - 1) * page_size result = await db.execute( select(User) .order_by(User.created_at.desc()) .offset(offset) .limit(page_size) ) users = list(result.scalars().all()) return users, total async def update_user( db: AsyncSession, user_id: uuid.UUID, **updates, ) -> Optional[User]: """Update user fields. Returns updated user or None if not found.""" user = await get_user_by_id(db, user_id) if user is None: return None for field, value in updates.items(): if field == "role" and value is not None: value = UserRole(value) if field == "password": user.password_hash = hash_password(value) elif hasattr(user, field) and value is not None: setattr(user, field, value) await db.flush() await db.refresh(user) return user async def deactivate_user(db: AsyncSession, user_id: uuid.UUID) -> Optional[User]: """Soft-delete a user by setting is_active=False. Returns user or None.""" user = await get_user_by_id(db, user_id) if user is None: return None user.is_active = False await db.flush() await db.refresh(user) return user