aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
167 lines
6.4 KiB
Python
167 lines
6.4 KiB
Python
"""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()
|