"""Business logic för användarhantering.""" from typing import Optional from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from fastapi import HTTPException, status from app.models import User, UserRole, UserStatus, Tenant from app.schemas import UserCreate, UserUpdate from app.security import get_password_hash, verify_password, can_manage class UserService: def __init__(self, db: AsyncSession): self.db = db async def get_by_id(self, user_id: UUID) -> Optional[User]: result = await self.db.execute(select(User).where(User.id == user_id)) return result.scalar_one_or_none() async def get_by_email(self, email: str) -> Optional[User]: result = await self.db.execute(select(User).where(User.email == email.lower().strip())) return result.scalar_one_or_none() async def list_users( self, tenant_id: Optional[UUID] = None, role: Optional[UserRole] = None, status: Optional[UserStatus] = None, skip: int = 0, limit: int = 100, ) -> tuple[list[User], int]: query = select(User) count_query = select(func.count()).select_from(User) if tenant_id: query = query.where(User.tenant_id == tenant_id) count_query = count_query.where(User.tenant_id == tenant_id) if role: query = query.where(User.role == role) count_query = count_query.where(User.role == role) if status: query = query.where(User.status == status) count_query = count_query.where(User.status == status) total_result = await self.db.execute(count_query) total = total_result.scalar_one() result = await self.db.execute(query.offset(skip).limit(limit).order_by(User.created_at.desc())) return result.scalars().all(), total async def create_user(self, data: UserCreate, created_by: Optional[User] = None) -> User: existing = await self.get_by_email(data.email) if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="User with this email already exists", ) if data.tenant_id: tenant = await self.db.execute(select(Tenant).where(Tenant.id == data.tenant_id)) if tenant.scalar_one_or_none() is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Tenant not found", ) user = User( email=data.email.lower().strip(), first_name=data.first_name.strip(), last_name=data.last_name.strip(), role=data.role, tenant_id=data.tenant_id, status=UserStatus.ACTIVE if data.password else UserStatus.PENDING, hashed_password=get_password_hash(data.password) if data.password else None, ) self.db.add(user) await self.db.commit() await self.db.refresh(user) return user async def update_user(self, user_id: UUID, data: UserUpdate, actor: User) -> User: user = await self.get_by_id(user_id) if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") if not actor.is_superuser and actor.id != user_id: if not can_manage(actor.role, user.role): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot modify user with equal or higher role", ) if data.email and data.email.lower().strip() != user.email: existing = await self.get_by_email(data.email) if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Email already in use", ) user.email = data.email.lower().strip() if data.first_name is not None: user.first_name = data.first_name.strip() if data.last_name is not None: user.last_name = data.last_name.strip() if data.role is not None: if not actor.is_superuser and not can_manage(actor.role, data.role): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot assign role equal or higher than your own", ) user.role = data.role if data.status is not None: user.status = data.status if data.tenant_id is not None: user.tenant_id = data.tenant_id await self.db.commit() await self.db.refresh(user) return user async def delete_user(self, user_id: UUID, actor: User) -> None: user = await self.get_by_id(user_id) if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") if not actor.is_superuser: if not can_manage(actor.role, user.role): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot delete user with equal or higher role", ) if actor.id == user_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete yourself", ) await self.db.delete(user) await self.db.commit() async def authenticate(self, email: str, password: str) -> Optional[User]: user = await self.get_by_email(email) if not user or not user.hashed_password: return None if not verify_password(password, user.hashed_password): return None if user.status != UserStatus.ACTIVE: return None return user async def set_password(self, user: User, password: str) -> None: user.hashed_password = get_password_hash(password) user.status = UserStatus.ACTIVE await self.db.commit() async def change_password(self, user: User, current_password: str, new_password: str) -> None: if not verify_password(current_password, user.hashed_password): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Current password is incorrect", ) user.hashed_password = get_password_hash(new_password) await self.db.commit()