first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,258 @@
|
||||
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)
|
||||
Reference in new issue
Block a user