from django.contrib.auth import authenticate, login, logout from django.core.exceptions import ValidationError from django.middleware.csrf import get_token from django.views.decorators.csrf import ensure_csrf_cookie from django.utils.decorators import method_decorator from rest_framework import permissions, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from apps.accounts.models import ApiKey, User from apps.accounts.permissions import IsExecutive from apps.accounts.scoping import filter_users_queryset from apps.accounts.serializers import ( ApiKeyCreateSerializer, ApiKeySerializer, LoginSerializer, SetPasswordSerializer, UserSerializer, UserWriteSerializer, ) def _validation_detail(exc: ValidationError): if hasattr(exc, "message_dict"): return exc.message_dict if hasattr(exc, "messages"): return "; ".join(str(m) for m in exc.messages) return str(getattr(exc, "message", exc)) class IsOwnerOrStaff(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.user.is_superuser: return True return getattr(obj, "user_id", None) == request.user.pk or obj == request.user class IsAdminUser(permissions.BasePermission): """Write access for Account Admin or Django superuser break-glass.""" def has_permission(self, request, view): from apps.accounts.permissions import can_manage_users return can_manage_users(request.user) class UserViewSet(viewsets.ModelViewSet): """ List/retrieve: scoped for executives (BUH → managed GMs; director / account_admin → all). Create/update/delete/set-password: Account Admin or superuser. GM/GM Admin can request a BUH; that BUH can approve/reject. """ serializer_class = UserSerializer def get_queryset(self): qs = User.objects.select_related("managed_by", "managed_by_pending").order_by( "display_name" ) return filter_users_queryset(qs, self.request.user) def get_serializer_class(self): if self.action in {"create", "update", "partial_update"}: return UserWriteSerializer return UserSerializer def get_permissions(self): if self.action in { "list", "retrieve", "buhs", "pending_managed", "request_managed_by", "cancel_managed_by_request", "approve_managed_by", "reject_managed_by", }: return [IsExecutive()] return [IsAdminUser()] @action(detail=True, methods=["post"], url_path="set-password") def set_password(self, request, pk=None): user = self.get_object() serializer = SetPasswordSerializer(data=request.data) serializer.is_valid(raise_exception=True) user.set_password(serializer.validated_data["password"]) user.save(update_fields=["password", "updated_at"]) return Response(UserSerializer(user).data) @action(detail=False, methods=["get"], url_path="buhs") def buhs(self, request): """List BUH accounts a GM can request to report under (not BU Admin).""" if request.user.status != User.STATUS_GM and not request.user.is_superuser: if request.user.status not in { User.STATUS_DIRECTOR, User.STATUS_DIRECTOR_ADMIN, User.STATUS_BUH, User.STATUS_BU_ADMIN, }: return Response({"detail": "Not allowed."}, status=status.HTTP_403_FORBIDDEN) rows = User.objects.filter( status=User.STATUS_BUH, is_active=True, ).order_by("display_name") return Response(UserSerializer(rows, many=True).data) @action(detail=False, methods=["get"], url_path="pending-managed") def pending_managed(self, request): """BUH only: GMs who requested to report under this account.""" if request.user.status != User.STATUS_BUH: return Response({"detail": "Not allowed."}, status=status.HTTP_403_FORBIDDEN) rows = User.objects.filter( managed_by_pending=request.user, status=User.STATUS_GM, ).select_related("managed_by", "managed_by_pending").order_by("display_name") return Response(UserSerializer(rows, many=True).data) @action(detail=False, methods=["post"], url_path="request-managed-by") def request_managed_by(self, request): """GM only: request to report under a BUH (not BU Admin).""" if request.user.status != User.STATUS_GM: return Response({"detail": "Not allowed."}, status=status.HTTP_403_FORBIDDEN) buh_id = request.data.get("buh_id") try: buh = User.objects.get( pk=buh_id, status=User.STATUS_BUH, is_active=True, ) except (User.DoesNotExist, TypeError, ValueError): return Response({"detail": "Invalid buh_id."}, status=status.HTTP_400_BAD_REQUEST) try: request.user.request_managed_by(buh) except ValidationError as exc: return Response({"detail": _validation_detail(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(UserSerializer(request.user).data) @action(detail=False, methods=["post"], url_path="cancel-managed-by-request") def cancel_managed_by_request(self, request): if request.user.status != User.STATUS_GM: return Response({"detail": "Not allowed."}, status=status.HTTP_403_FORBIDDEN) request.user.cancel_managed_by_request() return Response(UserSerializer(request.user).data) @action(detail=True, methods=["post"], url_path="approve-managed-by") def approve_managed_by(self, request, pk=None): if request.user.status != User.STATUS_BUH: return Response({"detail": "Not allowed."}, status=status.HTTP_403_FORBIDDEN) gm = User.objects.filter( pk=pk, managed_by_pending=request.user, status=User.STATUS_GM, ).first() if gm is None: return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND) try: gm.approve_managed_by(request.user) except ValidationError as exc: return Response({"detail": _validation_detail(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(UserSerializer(gm).data) @action(detail=True, methods=["post"], url_path="reject-managed-by") def reject_managed_by(self, request, pk=None): if request.user.status != User.STATUS_BUH: return Response({"detail": "Not allowed."}, status=status.HTTP_403_FORBIDDEN) gm = User.objects.filter( pk=pk, managed_by_pending=request.user, status=User.STATUS_GM, ).first() if gm is None: return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND) try: gm.reject_managed_by(request.user) except ValidationError as exc: return Response({"detail": _validation_detail(exc)}, status=status.HTTP_400_BAD_REQUEST) return Response(UserSerializer(gm).data) class ApiKeyViewSet(viewsets.ModelViewSet): http_method_names = ["get", "post", "delete", "head", "options"] def get_queryset(self): qs = ApiKey.objects.select_related("user").order_by("-created_at") if self.request.user.is_superuser: return qs return qs.filter(user=self.request.user) def get_serializer_class(self): if self.action == "create": return ApiKeyCreateSerializer return ApiKeySerializer def get_permissions(self): return [permissions.IsAuthenticated()] def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) name = serializer.validated_data["name"] user = request.user if request.user.is_superuser and serializer.validated_data.get("user_id"): user = User.objects.get(pk=serializer.validated_data["user_id"]) api_key, raw = ApiKey.generate(user=user, name=name) data = ApiKeySerializer(api_key).data data["raw_key"] = raw return Response(data, status=status.HTTP_201_CREATED) def perform_destroy(self, instance): if ( not self.request.user.is_superuser and instance.user_id != self.request.user.pk ): raise permissions.PermissionDenied() instance.delete() @method_decorator(ensure_csrf_cookie, name="dispatch") class CsrfView(APIView): permission_classes = [permissions.AllowAny] authentication_classes = [] def get(self, request): return Response({"csrfToken": get_token(request)}) class LoginView(APIView): permission_classes = [permissions.AllowAny] authentication_classes = [] def post(self, request): serializer = LoginSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = authenticate( request, username=serializer.validated_data["user_name"], password=serializer.validated_data["password"], ) if user is None: return Response( {"detail": "Invalid credentials"}, status=status.HTTP_401_UNAUTHORIZED, ) login(request, user) return Response(UserSerializer(user).data) class LogoutView(APIView): def post(self, request): logout(request) return Response(status=status.HTTP_204_NO_CONTENT) class MeView(APIView): def get(self, request): return Response(UserSerializer(request.user).data)