Files
boc/landvex-admin-backend/app/services/invitation_service.py
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

156 lines
5.8 KiB
Python

"""Business logic för inbjudningsflödet."""
import secrets
from datetime import datetime, timedelta
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 Invitation, User, UserRole, UserStatus, Tenant
from app.schemas import InvitationCreate, AcceptInvitationRequest
from app.config import get_settings
from app.security import get_password_hash, can_manage
from app.services.user_service import UserService
settings = get_settings()
class InvitationService:
def __init__(self, db: AsyncSession):
self.db = db
self.user_service = UserService(db)
async def get_by_token(self, token: str) -> Optional[Invitation]:
result = await self.db.execute(select(Invitation).where(Invitation.token == token))
return result.scalar_one_or_none()
async def list_invitations(
self,
tenant_id: Optional[UUID] = None,
pending_only: bool = True,
skip: int = 0,
limit: int = 100,
) -> tuple[list[Invitation], int]:
query = select(Invitation)
count_query = select(func.count()).select_from(Invitation)
if tenant_id:
query = query.where(Invitation.tenant_id == tenant_id)
count_query = count_query.where(Invitation.tenant_id == tenant_id)
if pending_only:
query = query.where(Invitation.accepted_at.is_(None))
count_query = count_query.where(Invitation.accepted_at.is_(None))
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(Invitation.created_at.desc()))
return result.scalars().all(), total
async def create_invitation(self, data: InvitationCreate, invited_by: User) -> Invitation:
# Validera att inviteraren kan ge denna roll
if not invited_by.is_superuser and not can_manage(invited_by.role, data.role):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot invite with role equal or higher than your own",
)
# Kolla om användaren redan finns
existing_user = await self.user_service.get_by_email(data.email)
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with this email already exists",
)
# Kolla om det redan finns en aktiv inbjudan
existing_invite = await self.db.execute(
select(Invitation).where(
Invitation.email == data.email.lower().strip(),
Invitation.accepted_at.is_(None),
Invitation.expires_at > datetime.utcnow(),
)
)
if existing_invite.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Active invitation already exists for this email",
)
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",
)
token = secrets.token_urlsafe(48)
invitation = Invitation(
email=data.email.lower().strip(),
token=token,
role=data.role,
tenant_id=data.tenant_id,
invited_by_id=invited_by.id,
expires_at=datetime.utcnow() + timedelta(hours=settings.INVITATION_TOKEN_EXPIRE_HOURS),
)
self.db.add(invitation)
await self.db.commit()
await self.db.refresh(invitation)
return invitation
async def accept_invitation(self, data: AcceptInvitationRequest) -> User:
invitation = await self.get_by_token(data.token)
if invitation is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invitation not found",
)
if invitation.accepted_at is not None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation already accepted",
)
if invitation.expires_at < datetime.utcnow():
raise HTTPException(
status_code=status.HTTP_410_GONE,
detail="Invitation has expired",
)
# Skapa användaren
user = User(
email=invitation.email,
first_name=data.first_name.strip(),
last_name=data.last_name.strip(),
role=invitation.role,
tenant_id=invitation.tenant_id,
status=UserStatus.ACTIVE,
hashed_password=get_password_hash(data.password),
)
self.db.add(user)
invitation.accepted_at = datetime.utcnow()
await self.db.commit()
await self.db.refresh(user)
return user
async def revoke_invitation(self, invitation_id: UUID, actor: User) -> None:
invitation = await self.db.execute(select(Invitation).where(Invitation.id == invitation_id))
inv = invitation.scalar_one_or_none()
if inv is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invitation not found")
if not actor.is_superuser and inv.invited_by_id != actor.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only revoke invitations you sent",
)
await self.db.delete(inv)
await self.db.commit()
def get_invitation_link(self, token: str) -> str:
return f"{settings.FRONTEND_SET_PASSWORD_URL}?token={token}"