259 lines
9.7 KiB
Python
259 lines
9.7 KiB
Python
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(
|
|
"user_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("user_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("user_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)
|