from django.shortcuts import get_object_or_404 from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from infrasynth.security.permissions import IsAuthenticatedAndPermitted from infrasynth.shared.exceptions import NotFoundError, ValidationAppError from .filters import TenantFilter, TenantInvitationFilter, TenantMembershipFilter from .models import Tenant, TenantMembership from .serializers import ( TenantInvitationSerializer, TenantMembershipSerializer, TenantSerializer, ) from .services import TenantService class TenantViewSet(viewsets.ModelViewSet): serializer_class = TenantSerializer permission_classes = [IsAuthenticatedAndPermitted] filterset_class = TenantFilter def get_queryset(self): return ( Tenant.objects.filter(memberships__user=self.request.user, memberships__is_active=True) .distinct() .order_by("name") ) def perform_create(self, serializer): tenant = TenantService().create_tenant( serializer.validated_data["name"], self.request.user, slug=serializer.validated_data.get("slug"), ) serializer.instance = tenant def perform_update(self, serializer): validated = serializer.validated_data changes = { field: validated[field] for field in ("name", "locale", "timezone", "metadata") if field in validated } TenantService().update_tenant(serializer.instance, actor=self.request.user, **changes) @action(detail=True, methods=["get"], url_path="members") def members(self, request, pk=None): tenant = self.get_object() memberships = TenantMembership.objects.filter(tenant=tenant).select_related("user") return Response(TenantMembershipSerializer(memberships, many=True).data) @action(detail=True, methods=["post"], url_path="members/invite") def invite(self, request, pk=None): tenant = self.get_object() email = request.data.get("email") if not email: raise ValidationAppError("email is required.", code="VALIDATION_FIELD_REQUIRED") invitation = TenantService().invite( tenant, email, role=request.data.get("role", "member"), invited_by=request.user ) return Response(TenantInvitationSerializer(invitation).data, status=status.HTTP_201_CREATED) @action( detail=True, methods=["delete"], url_path=r"members/(?P[^/.]+)", ) def remove_member(self, request, pk=None, membership_id=None): tenant = self.get_object() membership = get_object_or_404(TenantMembership, tenant=tenant, pk=membership_id) TenantService().remove_member(tenant, membership.user) return Response(status=status.HTTP_204_NO_CONTENT) class TenantMembershipViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet, ): serializer_class = TenantMembershipSerializer permission_classes = [IsAuthenticatedAndPermitted] filterset_class = TenantMembershipFilter def get_queryset(self): return ( TenantMembership.objects.filter( tenant__memberships__user=self.request.user, tenant__memberships__is_active=True, ) .select_related("tenant", "user") .distinct() ) class TenantInvitationViewSet(viewsets.GenericViewSet): serializer_class = TenantInvitationSerializer permission_classes = [IsAuthenticatedAndPermitted] filterset_class = TenantInvitationFilter @action(detail=False, methods=["post"], url_path="accept") def accept(self, request): token = request.data.get("token") if not token: raise ValidationAppError("token is required.", code="VALIDATION_FIELD_REQUIRED") try: membership = TenantService().accept_invitation(token, request.user) except NotFoundError as exc: return Response(exc.to_dict(), status=exc.status) return Response(TenantMembershipSerializer(membership).data, status=status.HTTP_200_OK)