"""Serializers for registration, login and profile of the API's own users.""" from __future__ import annotations import logging from typing import Any from django.contrib.auth import password_validation from django.db import IntegrityError from rest_framework import serializers from rest_framework_simplejwt.serializers import ( TokenObtainPairSerializer as BaseTokenObtainPairSerializer, ) from apps.accounts.models import Role, User audit = logging.getLogger("audit.auth") class UserSerializer(serializers.ModelSerializer): can_view_pii = serializers.BooleanField(read_only=True) class Meta: model = User fields = ("id", "email", "full_name", "role", "can_view_pii", "date_joined") read_only_fields = fields class RegisterSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, style={"input_type": "password"}) password_confirm = serializers.CharField(write_only=True, style={"input_type": "password"}) class Meta: model = User fields = ("email", "full_name", "password", "password_confirm") def validate_email(self, value: str) -> str: return value.strip().lower() def validate_password(self, value: str) -> str: # Runs Django's configured validators: length, common passwords, etc. password_validation.validate_password(value) return value def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: if attrs["password"] != attrs["password_confirm"]: raise serializers.ValidationError({"password_confirm": "Passwords do not match."}) return attrs def create(self, validated_data: dict[str, Any]) -> User: validated_data.pop("password_confirm") password = validated_data.pop("password") try: # Self-service sign-up always yields the least privileged role; # elevation is an explicit admin action. return User.objects.create_user(password=password, role=Role.VIEWER, **validated_data) except IntegrityError as exc: # pragma: no cover - race with unique index raise serializers.ValidationError( {"email": "A user with this email already exists."} ) from exc class TokenObtainPairSerializer(BaseTokenObtainPairSerializer): """Adds identity claims to the access token and records an audit event.""" @classmethod def get_token(cls, user: User): # noqa: ANN206 token = super().get_token(user) token["role"] = user.role token["email"] = user.email return token def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: data = super().validate(attrs) audit.info( "login_succeeded", extra={ "event": "login_succeeded", "user_id": self.user.id, "user_role": self.user.role, }, ) data["user"] = UserSerializer(self.user).data return data class LogoutSerializer(serializers.Serializer): refresh = serializers.CharField()