Files
dashboard-cpsp-executive/backend/apps/accounts/views.py
T
2026-09-08 15:21:23 +07:00

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)